Files
bea-edit/tests/e2e/bea.spec.ts
T
2026-10-06 22:52:35 +02:00

492 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from '@playwright/test';
import { zipSync } from 'fflate';
import {
appendZipInput,
createMultiFileZip,
createPdf,
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 Datei · 1 Archiv geöffnet/);
await replaceThroughDialog(page, zipB, false);
await expect(workspaceStatus(page, /1 Datei · 1 Archiv geöffnet/)).toBeVisible();
await expect(page.getByText('2 Dateien')).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 Datei · 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 Datei · 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 Datei · 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 Datei · 1 Archiv geöffnet/);
await replaceThroughDialog(page, zipB, true);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await replaceThroughDialog(page, zipB, true);
await waitForLoadedWorkspace(page, /1 Datei · 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 Datei · 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 Dateien · 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 Datei · 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 Dateien · 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 Datei · 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 Datei · 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 Datei · 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 Datei · 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 Datei · 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 Datei · 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 Datei 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 Datei · 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([]);
});
test('grouping mode creates Teildokumente and never duplicates their names', async ({ page }) => {
const pageErrors = trackPageErrors(page);
const zip = await createMultiFileZip(['antrag', 'klage', 'gutachten']);
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'archiv.zip', mimeType: 'application/zip', buffer: zip }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await page.getByRole('button', { name: 'Gruppieren' }).click();
const createButton = page.getByRole('button', { name: 'Teildokument erstellen' });
await expect(page.getByText('0 Dateien ausgewählt')).toBeVisible();
await expect(createButton).toBeDisabled();
const checkboxes = page.locator('li input[type="checkbox"]');
await checkboxes.nth(0).check();
await expect(page.getByText('1 Datei ausgewählt')).toBeVisible();
await createButton.click();
// the new block appears, is named "Teildokument 1", and its name input is focused
await expect(page.getByText('2 lose Dateien · 1 Teildokument')).toBeVisible();
const nameInput = page.getByLabel('Teildokumentname');
await expect(nameInput).toHaveValue('Teildokument 1');
await expect(nameInput).toBeFocused();
// create a second block, dissolve the first, then create again:
// the next number must not collide with the surviving "Teildokument 2"
await checkboxes.nth(0).check();
await createButton.click();
await expect(page.getByLabel('Teildokumentname').nth(1)).toHaveValue('Teildokument 2');
await page.getByRole('button', { name: 'Auflösen' }).first().click();
await expect(page.getByText('2 lose Dateien')).toBeVisible();
await checkboxes.nth(0).check();
await createButton.click();
await expect(page.getByLabel('Teildokumentname').nth(1)).toHaveValue('Teildokument 3');
await page.keyboard.press('Escape');
await expect(page.getByRole('button', { name: 'Fertig' })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Gruppieren' })).toBeVisible();
expect(pageErrors).toEqual([]);
});
test('grouping mode moves a selection into an existing Teildokument', async ({ page }) => {
const pageErrors = trackPageErrors(page);
const zip = await createMultiFileZip(['antrag', 'klage', 'gutachten']);
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'archiv.zip', mimeType: 'application/zip', buffer: zip }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await page.getByRole('button', { name: 'Neues Teildokument' }).click();
await expect(page.getByLabel('Teildokumentname')).toHaveValue('Teildokument 1');
await page.getByRole('button', { name: 'Gruppieren' }).click();
const checkboxes = page.locator('li input[type="checkbox"]');
await checkboxes.nth(0).check();
await checkboxes.nth(1).check();
await expect(page.getByText('2 Dateien ausgewählt')).toBeVisible();
const moveSelect = page.getByLabel('Auswahl in Teildokument verschieben');
await moveSelect.selectOption({ label: 'Teildokument 1' });
await expect(page.getByText('1 lose Datei · 1 Teildokument')).toBeVisible();
await expect(page.getByText('2 Dateien').first()).toBeVisible();
expect(pageErrors).toEqual([]);
});
test('attachment menu moves files between sub-documents and the loose list', async ({ page }) => {
const pageErrors = trackPageErrors(page);
const zip = await createMultiFileZip(['antrag', 'klage', 'gutachten']);
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'archiv.zip', mimeType: 'application/zip', buffer: zip }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
// create a sub-document from a loose file without drag-and-drop (touch path)
await page.getByRole('button', { name: 'Aktionen für antrag.pdf' }).click();
await page.getByRole('menuitem', { name: 'In neues Teildokument verschieben' }).click();
await expect(page.getByText('2 lose Dateien · 1 Teildokument')).toBeVisible();
// move a second loose file into the existing sub-document
await page.getByRole('button', { name: 'Aktionen für klage.pdf' }).click();
await page.getByRole('menuitem', { name: 'Nach „Teildokument 1“ verschieben' }).click();
await expect(page.getByText('1 lose Datei · 1 Teildokument')).toBeVisible();
// move it back out to the loose list
await page.getByRole('button', { name: 'Aktionen für klage.pdf' }).click();
await page.getByRole('menuitem', { name: 'Zu den losen Dateien verschieben' }).click();
await expect(page.getByText('2 lose Dateien · 1 Teildokument')).toBeVisible();
// remove a loose file with confirmation
await page.getByRole('button', { name: 'Aktionen für gutachten.pdf' }).click();
await page.getByRole('menuitem', { name: 'Entfernen: gutachten.pdf' }).click();
await page.getByRole('button', { name: 'Entfernen', exact: true }).click();
await expect(page.getByText('1 lose Datei · 1 Teildokument')).toBeVisible();
expect(pageErrors).toEqual([]);
});
test('quick action groups every loose file as its own Teildokument', async ({ page }) => {
const pageErrors = trackPageErrors(page);
const zip = await createMultiFileZip(['antrag', 'klage']);
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'archiv.zip', mimeType: 'application/zip', buffer: zip }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await page.getByRole('button', { name: 'Jede Datei als Teildokument' }).click();
await expect(page.getByText('0 lose Dateien · 2 Teildokumente')).toBeVisible();
await expect(page.getByLabel('Teildokumentname').nth(0)).toHaveValue('antrag');
await expect(page.getByLabel('Teildokumentname').nth(1)).toHaveValue('klage');
expect(pageErrors).toEqual([]);
});
test('splitting a PDF attachment cuts the selected pages into a new Teildokument', async ({
page
}) => {
const pageErrors = trackPageErrors(page);
const zip = Buffer.from(zipSync({ 'vertrag.pdf': await createPdf(3, 'Vertrag') }));
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'archiv.zip', mimeType: 'application/zip', buffer: zip }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await page.getByRole('button', { name: 'Aktionen für vertrag.pdf' }).click();
await page.getByRole('menuitem', { name: 'Teilen: vertrag.pdf' }).click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await page.getByRole('button', { name: 'Seite 1' }).click();
await page.getByRole('button', { name: 'Seite 2' }).click();
await expect(page.getByText('2 von 3 Seiten ausgewählt')).toBeVisible();
await page.getByRole('button', { name: 'Als Teildokument übernehmen' }).click();
await expect(page.getByText('✓ Zugewiesen')).toHaveCount(2);
await page.locator('[data-split-apply]').click();
await expect(dialog).toHaveCount(0);
// the remaining page 3 stays loose, the cut pages form a new Teildokument
await expect(page.getByText('1 lose Datei · 1 Teildokument')).toBeVisible();
await expect(page.getByLabel('Teildokumentname')).toHaveValue('Seiten 1–2');
await expect(page.getByText('vertrag_Seiten_1-2.pdf')).toBeVisible();
expect(pageErrors).toEqual([]);
});
test('bulk split creates one Teildokument per page and removes the source', async ({ page }) => {
const pageErrors = trackPageErrors(page);
const zip = Buffer.from(zipSync({ 'vertrag.pdf': await createPdf(3, 'Vertrag') }));
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'archiv.zip', mimeType: 'application/zip', buffer: zip }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await page.getByRole('button', { name: 'Aktionen für vertrag.pdf' }).click();
await page.getByRole('menuitem', { name: 'Teilen: vertrag.pdf' }).click();
await page.getByText('Jede Seite einzeln').click();
await expect(page.getByText('Es werden 3 Teildokumente erstellt.')).toBeVisible();
await page.locator('[data-split-apply]').click();
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(page.getByText('0 lose Dateien · 3 Teildokumente')).toBeVisible();
await expect(page.getByLabel('Teildokumentname').nth(0)).toHaveValue('Seite 1');
await expect(page.getByLabel('Teildokumentname').nth(2)).toHaveValue('Seite 3');
expect(pageErrors).toEqual([]);
});
test('a dropped PDF opens as a workspace archive and can be split', async ({ page }) => {
const pageErrors = trackPageErrors(page);
const pdf = await createPdf(2, 'Antrag');
await page.goto('/tools/bea');
await dropFiles(page, zipDropzone(page), [
{ name: 'antrag.pdf', mimeType: 'application/pdf', buffer: pdf }
]);
await waitForLoadedWorkspace(page, /1 Datei · 1 Archiv geöffnet/);
await expect(page.getByText('1 lose Datei')).toBeVisible();
await page.getByRole('button', { name: 'Aktionen für antrag.pdf' }).click();
await page.getByRole('menuitem', { name: 'Teilen: antrag.pdf' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.getByRole('button', { name: 'Seite 1' }).click();
await page.getByRole('button', { name: 'Als Teildokument übernehmen' }).click();
await page.locator('[data-split-apply]').click();
await expect(page.getByText('1 Teildokument')).toBeVisible();
expect(pageErrors).toEqual([]);
});