547 lines
21 KiB
TypeScript
547 lines
21 KiB
TypeScript
import { expect, test, type Download, type Locator, type Page } from '@playwright/test';
|
||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||
import { zipSync } from 'fflate';
|
||
import { readFile } from 'node:fs/promises';
|
||
|
||
const createPdf = async (pageCount: number, label: string) => {
|
||
const document = await PDFDocument.create();
|
||
const font = await document.embedFont(StandardFonts.Helvetica);
|
||
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {
|
||
const page = document.addPage([420, 594]);
|
||
page.drawText(`${label} – Seite ${pageNumber}`, {
|
||
x: 40,
|
||
y: 520,
|
||
size: 20,
|
||
font,
|
||
color: rgb(0.1, 0.2, 0.4)
|
||
});
|
||
}
|
||
return Buffer.from(await document.save());
|
||
};
|
||
|
||
const dropFiles = async (
|
||
page: Page,
|
||
target: Locator | 'window',
|
||
files: Array<{ name: string; mimeType: string; buffer: Buffer }>
|
||
) => {
|
||
await page.locator('[data-dropzone-ready="true"]').first().waitFor();
|
||
const dataTransfer = await page.evaluateHandle(
|
||
(droppedFiles) => {
|
||
const transfer = new DataTransfer();
|
||
for (const droppedFile of droppedFiles) {
|
||
const bytes = Uint8Array.from(atob(droppedFile.base64), (character) =>
|
||
character.charCodeAt(0)
|
||
);
|
||
transfer.items.add(
|
||
new File([bytes], droppedFile.name, {
|
||
type: droppedFile.mimeType,
|
||
lastModified: Date.now()
|
||
})
|
||
);
|
||
}
|
||
return transfer;
|
||
},
|
||
files.map((file) => ({
|
||
name: file.name,
|
||
mimeType: file.mimeType,
|
||
base64: file.buffer.toString('base64')
|
||
}))
|
||
);
|
||
|
||
if (target === 'window') {
|
||
await page.evaluate((transfer) => {
|
||
window.dispatchEvent(
|
||
new DragEvent('drop', { dataTransfer: transfer, bubbles: true, cancelable: true })
|
||
);
|
||
}, dataTransfer);
|
||
} else {
|
||
await target.dispatchEvent('drop', { dataTransfer });
|
||
}
|
||
await dataTransfer.dispose();
|
||
};
|
||
|
||
const expectPdfDownload = async (download: Download, name: RegExp) => {
|
||
expect(download.suggestedFilename()).toMatch(name);
|
||
const path = await download.path();
|
||
expect(path).not.toBeNull();
|
||
const bytes = await readFile(path!);
|
||
expect(bytes.subarray(0, 5).toString()).toBe('%PDF-');
|
||
return bytes;
|
||
};
|
||
|
||
const trackPageErrors = (page: Page) => {
|
||
const errors: Error[] = [];
|
||
page.on('pageerror', (error) => errors.push(error));
|
||
return errors;
|
||
};
|
||
|
||
const pdfDropzone = (page: Page) => page.getByRole('button', { name: 'PDF-Dateien hinzufügen' });
|
||
const zipDropzone = (page: Page) =>
|
||
page.getByRole('button', { name: 'beA-ZIP-Dateien hinzufügen' });
|
||
|
||
const createZip = async (label: string, pageCount = 1) =>
|
||
Buffer.from(zipSync({ [`${label}.pdf`]: await createPdf(pageCount, label) }));
|
||
|
||
const newZipButton = (page: Page) => page.getByRole('button', { name: 'Neue ZIP bearbeiten' });
|
||
const settingsButton = (page: Page) =>
|
||
page.getByRole('button', { name: 'Einstellungen', exact: true });
|
||
const replaceZipInput = (page: Page) => page.locator('input[data-zip-picker="replace-zip"]');
|
||
const appendZipInput = (page: Page) => page.locator('input[data-zip-picker="append-zip"]');
|
||
const workspaceStatus = (page: Page, pattern: RegExp) => page.getByText(pattern);
|
||
|
||
const waitForLoadedWorkspace = async (
|
||
page: Page,
|
||
status: RegExp,
|
||
options?: { timeout?: number }
|
||
) => {
|
||
await expect(newZipButton(page)).toBeVisible();
|
||
await expect(workspaceStatus(page, status)).toBeVisible({ timeout: options?.timeout ?? 15_000 });
|
||
};
|
||
|
||
const replaceThroughDialog = async (page: Page, zip: Buffer, confirm: boolean) => {
|
||
await newZipButton(page).click();
|
||
await replaceZipInput(page).setInputFiles([
|
||
{ name: 'archiv-b.zip', mimeType: 'application/zip', buffer: zip }
|
||
]);
|
||
await expect(page.getByText('Aktuelle Bearbeitung ersetzen?')).toBeVisible();
|
||
if (confirm) {
|
||
await page.getByRole('button', { name: 'Neue ZIP öffnen' }).click();
|
||
} else {
|
||
await page.getByRole('button', { name: 'Abbrechen' }).click();
|
||
}
|
||
await expect(page.getByText('Aktuelle Bearbeitung ersetzen?')).toBeHidden();
|
||
};
|
||
|
||
test('start page exposes only working tools and forwards a dropped ZIP to beA', async ({
|
||
page
|
||
}) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
const pdf = await createPdf(1, 'beA');
|
||
const zip = Buffer.from(zipSync({ 'anlage.pdf': pdf }));
|
||
|
||
await page.goto('/');
|
||
await expect(page.getByRole('heading', { name: 'Werkzeugkasten für beA' })).toBeVisible();
|
||
await expect(page.getByRole('link', { name: /Signieren/ })).toHaveCount(0);
|
||
|
||
await dropFiles(page, 'window', [
|
||
{ name: 'bea-archiv.zip', mimeType: 'application/zip', buffer: zip }
|
||
]);
|
||
|
||
await expect(page).toHaveURL(/\/tools\/bea$/);
|
||
await expect(page.getByText('1 lose Datei')).toBeVisible({ timeout: 15_000 });
|
||
expect(pageErrors).toEqual([]);
|
||
});
|
||
|
||
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('short tool pages keep the footer flush with the viewport bottom', async ({ page }) => {
|
||
const expectFooterAtViewportBottom = async (viewport: { width: number; height: number }) => {
|
||
await page.setViewportSize(viewport);
|
||
await page.goto('/tools/watermark');
|
||
|
||
const footerBottom = await page.locator('footer').evaluate((footer) => {
|
||
return footer.getBoundingClientRect().bottom;
|
||
});
|
||
|
||
expect(Math.abs(footerBottom - viewport.height)).toBeLessThanOrEqual(1);
|
||
};
|
||
|
||
await expectFooterAtViewportBottom({ width: 1440, height: 900 });
|
||
await expectFooterAtViewportBottom({ width: 390, height: 844 });
|
||
});
|
||
|
||
test('merge accepts dropped PDFs and downloads a merged PDF', async ({ page }) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
await page.goto('/tools/merge');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{ name: 'eins.pdf', mimeType: 'application/pdf', buffer: await createPdf(1, 'Eins') },
|
||
{ name: 'zwei.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Zwei') }
|
||
]);
|
||
|
||
await expect(page.getByText('2 Dateien ausgewählt')).toBeVisible();
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'PDFs zusammenfügen' }).click();
|
||
await expectPdfDownload(await downloadPromise, /^zusammengefuegt\.pdf$/);
|
||
expect(pageErrors).toEqual([]);
|
||
});
|
||
|
||
test('separate renders dropped PDF pages and removes a selected page', async ({ page }) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
await page.goto('/tools/separate');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{ name: 'seiten.pdf', mimeType: 'application/pdf', buffer: await createPdf(3, 'Trennen') }
|
||
]);
|
||
|
||
await expect(page.getByRole('button', { name: /Seite 1/ })).toBeVisible({ timeout: 15_000 });
|
||
await page.getByRole('button', { name: /Seite 2/ }).click();
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'Seiten entfernen & herunterladen' }).click();
|
||
await expectPdfDownload(await downloadPromise, /^seiten_ohne_seiten\.pdf$/);
|
||
expect(pageErrors).toEqual([]);
|
||
});
|
||
|
||
test('watermark accepts numeric controls and downloads text and image watermarks', async ({
|
||
page
|
||
}) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
await page.goto('/tools/watermark');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{ name: 'marke.pdf', mimeType: 'application/pdf', buffer: await createPdf(1, 'Marke') }
|
||
]);
|
||
|
||
await expect(page.getByRole('heading', { name: 'Wasserzeichen-Einstellungen' })).toBeVisible({
|
||
timeout: 15_000
|
||
});
|
||
await page.locator('#watermark-color').fill('#00ff00');
|
||
await page.locator('#watermark-opacity').fill('0.45');
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'Wasserzeichen anwenden & herunterladen' }).click();
|
||
await expectPdfDownload(await downloadPromise, /^marke_wasserzeichen\.pdf$/);
|
||
|
||
await page.getByLabel('Bild', { exact: true }).check();
|
||
await dropFiles(page, page.getByRole('button', { name: 'Wasserzeichenbild hinzufügen' }), [
|
||
{
|
||
name: 'logo.png',
|
||
mimeType: 'image/png',
|
||
buffer: Buffer.from(
|
||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Z5rAAAAAASUVORK5CYII=',
|
||
'base64'
|
||
)
|
||
}
|
||
]);
|
||
const imageDownloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'Wasserzeichen anwenden & herunterladen' }).click();
|
||
await expectPdfDownload(await imageDownloadPromise, /^marke_wasserzeichen\.pdf$/);
|
||
|
||
await expect(page.getByText('Fehler beim Anwenden')).toHaveCount(0);
|
||
expect(pageErrors).toEqual([]);
|
||
});
|
||
|
||
test('encrypt creates a protected PDF and decrypt rebuilds it with the password', async ({
|
||
page
|
||
}) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
await page.goto('/tools/encrypt');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{ name: 'geheim.pdf', mimeType: 'application/pdf', buffer: await createPdf(1, 'Geheim') }
|
||
]);
|
||
|
||
await page.locator('#encrypt-password').fill('test-passwort');
|
||
await page.locator('#encrypt-password-confirmation').fill('test-passwort');
|
||
const encryptedDownloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'Passwort setzen & herunterladen' }).click();
|
||
const encryptedDownload = await encryptedDownloadPromise;
|
||
const encryptedBytes = await expectPdfDownload(encryptedDownload, /^geheim_geschuetzt\.pdf$/);
|
||
|
||
await page.goto('/tools/decrypt');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{
|
||
name: 'geheim_geschuetzt.pdf',
|
||
mimeType: 'application/pdf',
|
||
buffer: encryptedBytes
|
||
}
|
||
]);
|
||
await page.locator('#decrypt-password').fill('test-passwort');
|
||
const decryptedDownloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'Passwort entfernen & herunterladen' }).click();
|
||
await expectPdfDownload(await decryptedDownloadPromise, /^geheim_geschuetzt_entsperrt\.pdf$/);
|
||
expect(pageErrors).toEqual([]);
|
||
});
|
||
|
||
test('compress previews result size before downloading', async ({ page }) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
await page.goto('/tools/compress');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{ name: 'gross.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Komprimieren') }
|
||
]);
|
||
|
||
await page.getByRole('button', { name: 'Komprimieren', exact: true }).click();
|
||
await expect(page.getByText(/Ergebnisgröße:/)).toBeVisible({ timeout: 20_000 });
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'PDF herunterladen' }).click();
|
||
await expectPdfDownload(await downloadPromise, /^gross_komprimiert\.pdf$/);
|
||
expect(pageErrors).toEqual([]);
|
||
});
|
||
|
||
test('convert bundles a multi-page PDF as a correctly named ZIP', async ({ page }) => {
|
||
const pageErrors = trackPageErrors(page);
|
||
await page.goto('/tools/convert');
|
||
await dropFiles(page, pdfDropzone(page), [
|
||
{ name: 'bilder.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Bilder') }
|
||
]);
|
||
|
||
const downloadPromise = page.waitForEvent('download');
|
||
await page.getByRole('button', { name: 'Konvertieren & herunterladen' }).click();
|
||
const download = await downloadPromise;
|
||
expect(download.suggestedFilename()).toBe('bilder_bilder.zip');
|
||
expect(download.suggestedFilename()).not.toContain('.pdf');
|
||
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([]);
|
||
});
|