import { expect, test } from '@playwright/test'; import { appendZipInput, createZip, dropFiles, expectPdfDownload, newZipButton, replaceThroughDialog, replaceZipInput, settingsButton, trackPageErrors, waitForLoadedWorkspace, workspaceStatus, zipDropzone } from './helpers'; test('beA reports an invalid ZIP and lets the user remove it', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'defektes-archiv.zip', mimeType: 'application/zip', buffer: Buffer.from('kein gueltiges ZIP-Archiv') } ]); await expect(page.getByText('defektes-archiv.zip', { exact: true })).toBeVisible(); await expect(page.getByText('Konnte nicht geöffnet werden.')).toBeVisible(); await expect(page.getByRole('status')).toContainText('1 fehlgeschlagen'); await expect(page.getByRole('button', { name: 'Erneut versuchen' })).toBeVisible(); await page.getByRole('button', { name: 'Entfernen' }).click(); await expect(zipDropzone(page)).toBeVisible(); expect(pageErrors).toEqual([]); }); test('cancelling the replacement confirmation keeps the current archive', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); const zipB = await createZip('archiv-b'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await replaceThroughDialog(page, zipB, false); await expect(workspaceStatus(page, /1 ZIP · 1 Archiv geöffnet/)).toBeVisible(); await expect(page.getByText('2 ZIPs')).toHaveCount(0); expect(pageErrors).toEqual([]); }); test('confirming the replacement opens only the new ZIP and drops stale results', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a', 3); const zipB = await createZip('archiv-b'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); // Replace while the first ZIP may still be extracting; generation guarding // must ensure no late result from archiv-a reappears. await replaceThroughDialog(page, zipB, true); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await expect(page.getByText('1 lose Datei')).toHaveCount(1); expect(pageErrors).toEqual([]); }); test('cancelling the system file picker leaves the workspace unchanged', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await newZipButton(page).click(); await replaceZipInput(page).setInputFiles([]); await expect(page.getByText('Aktuelle Bearbeitung ersetzen?')).toHaveCount(0); await expect(workspaceStatus(page, /1 ZIP · 1 Archiv geöffnet/)).toBeVisible(); expect(pageErrors).toEqual([]); }); test('the replacement input accepts the same ZIP twice', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); const zipB = await createZip('archiv-b'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await replaceThroughDialog(page, zipB, true); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await replaceThroughDialog(page, zipB, true); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await expect(page.getByText('1 lose Datei')).toHaveCount(1); expect(pageErrors).toEqual([]); }); test('appending a ZIP keeps both archive editors visible', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); const zipB = await createZip('archiv-b'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await settingsButton(page).click(); await appendZipInput(page).setInputFiles([ { name: 'archiv-b.zip', mimeType: 'application/zip', buffer: zipB } ]); await waitForLoadedWorkspace(page, /2 ZIPs · 2 Archive geöffnet/); await expect(page.getByText('1 lose Datei')).toHaveCount(2); expect(pageErrors).toEqual([]); }); test('bulk export only appears for two or more processed archives', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); const zipB = await createZip('archiv-b'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await settingsButton(page).click(); await expect( page.getByRole('button', { name: 'Alle Archive als PDF herunterladen' }) ).toHaveCount(0); await appendZipInput(page).setInputFiles([ { name: 'archiv-b.zip', mimeType: 'application/zip', buffer: zipB } ]); await waitForLoadedWorkspace(page, /2 ZIPs · 2 Archive geöffnet/); // The popover stays open, so the new bulk export action appears in place. const exportButton = page.getByRole('button', { name: 'Alle Archive als PDF herunterladen' }); await expect(exportButton).toBeVisible(); const downloadPromise = page.waitForEvent('download'); await exportButton.click(); await expectPdfDownload(await downloadPromise, /\.pdf$/); expect(pageErrors).toEqual([]); }); test('clearing the workspace requires confirmation and returns to the dropzone', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await settingsButton(page).click(); await page.getByRole('button', { name: 'Aktuelle Bearbeitung leeren' }).click(); await expect(page.getByText('Aktuelle Bearbeitung leeren?')).toBeVisible(); await page.getByRole('button', { name: 'Abbrechen' }).click(); await expect(workspaceStatus(page, /1 ZIP · 1 Archiv geöffnet/)).toBeVisible(); await page.getByRole('button', { name: 'Aktuelle Bearbeitung leeren' }).click(); await page.getByRole('button', { name: 'Bearbeitung leeren', exact: true }).click(); await expect(zipDropzone(page)).toBeVisible(); await expect(newZipButton(page)).toHaveCount(0); expect(pageErrors).toEqual([]); }); test('the preview-size slider works with arrow keys and survives a reload', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await expect(page.locator('li[style*="width: 200px"]').first()).toBeVisible(); await settingsButton(page).click(); const slider = page.getByRole('slider', { name: 'Vorschaugröße' }); await expect(page.getByText('200 px')).toBeVisible(); await slider.focus(); await page.keyboard.press('ArrowRight'); await expect(page.getByText('201 px')).toBeVisible(); await expect(page.locator('li[style*="width: 201px"]').first()).toBeVisible(); await page.reload(); await page.locator('[data-dropzone-ready="true"]').first().waitFor(); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await settingsButton(page).click(); await expect(page.getByText('201 px')).toBeVisible(); await expect(await page.evaluate(() => localStorage.getItem('thumbnailWidth'))).toBe('201'); expect(pageErrors).toEqual([]); }); test('settings closes on Escape and outside click and returns focus to its trigger', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await expect(settingsButton(page)).toHaveAccessibleName('Einstellungen'); await settingsButton(page).click(); await expect(page.getByText('Vorschaugröße')).toBeVisible(); await page.keyboard.press('Escape'); await expect(page.getByText('Vorschaugröße')).toHaveCount(0); await expect(settingsButton(page)).toBeFocused(); await settingsButton(page).click(); await expect(page.getByText('Vorschaugröße')).toBeVisible(); await page.getByRole('heading', { name: 'beA-Edit' }).click(); await expect(page.getByText('Vorschaugröße')).toHaveCount(0); expect(pageErrors).toEqual([]); }); test('mobile toolbar and settings popover stay inside the viewport', async ({ page }) => { const pageErrors = trackPageErrors(page); const zipA = await createZip('archiv-a'); await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/tools/bea'); await dropFiles(page, zipDropzone(page), [ { name: 'archiv-a.zip', mimeType: 'application/zip', buffer: zipA } ]); await waitForLoadedWorkspace(page, /1 ZIP · 1 Archiv geöffnet/); await settingsButton(page).click(); const popover = page.locator('[data-slot="popover-content"]'); await expect(popover).toBeVisible(); await expect(page.getByText('Weitere ZIP hinzufügen')).toBeVisible(); const popoverBox = await popover.boundingBox(); expect(popoverBox).not.toBeNull(); expect(popoverBox!.x).toBeGreaterThanOrEqual(0); expect(popoverBox!.x + popoverBox!.width).toBeLessThanOrEqual(390); expect(popoverBox!.y).toBeGreaterThanOrEqual(0); expect(popoverBox!.y + popoverBox!.height).toBeLessThanOrEqual(844); const popoverMetrics = await popover.evaluate((element) => ({ scrollWidth: element.scrollWidth, clientWidth: element.clientWidth })); expect(popoverMetrics.scrollWidth).toBeLessThanOrEqual(popoverMetrics.clientWidth); // The toolbar sits in normal flow, so it must not cover the archive header. const toolbarBox = await workspaceStatus(page, /1 ZIP · 1 Archiv geöffnet/).boundingBox(); const archiveHeaderBox = await page.getByText('1 lose Datei').first().boundingBox(); expect(toolbarBox).not.toBeNull(); expect(archiveHeaderBox).not.toBeNull(); expect(toolbarBox!.y + toolbarBox!.height).toBeLessThanOrEqual(archiveHeaderBox!.y); expect(pageErrors).toEqual([]); });