Steuerkram/tests/e2e/helpers/app.js
fds2610-bot 7cea573ffc Fix #e2e: E2E-Tests ergänzt, /accounts/new und /assets/new lieferten HTTP 500
Steuerkram an den e2e-runner angebunden (tests/e2e/, 44 Playwright-Tests).
Der erste Smoke-Lauf fand zwei Seiten mit HTTP 500: AccountController::create
las $_GET['from'] ungeprüft, assets/form.php nutzte $testMode und
$defaultKontoId außerhalb des Bearbeiten-Zweigs. Beides war bis zur
Fehler-Härtung (#142) nur eine stille Warning.

Test 67 räumt sein Belegverzeichnis jetzt mit ab - es blieb als root-eigenes
Verzeichnis liegen und blockierte spätere echte Uploads mit HTTP 500.

Test 77 (neu) sichert die Löschsperre von Beleg::delete() ab.
2026-08-02 11:58:33 +02:00

182 lines
5.7 KiB
JavaScript

// @ts-check
// Gemeinsame Hilfsfunktionen für die Steuerkram-E2E-Tests.
//
// Grundsatz: Die Tests laufen gegen eine Instanz mit echten Arbeitsdaten.
// Sie legen deshalb ausschließlich eigene, eindeutig markierte Datensätze an
// (siehe uniqueTag) und räumen diese im Teardown wieder ab. Bestehende Daten
// werden nie verändert oder gelöscht.
const { expect } = require('@playwright/test')
/** Präfix, an dem sich Testdaten jederzeit erkennen und aufräumen lassen. */
const TAG_PREFIX = 'E2E-TEST'
let counter = 0
/** Eindeutiger Marker pro Datensatz, z.B. "E2E-TEST-1754130000000-3". */
function uniqueTag(suffix = '') {
counter += 1
return `${TAG_PREFIX}-${Date.now()}-${counter}${suffix ? '-' + suffix : ''}`
}
/**
* Minimales, gültiges PDF. Der Marker landet im Dateiinhalt, damit jeder
* Upload einen eigenen SHA-256-Hash hat — sonst greift die Dublettenprüfung.
*/
function pdfBuffer(marker) {
const pdf = [
'%PDF-1.4',
'1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj',
'2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj',
'3 0 obj<</Type/Page/MediaBox[0 0 300 200]/Parent 2 0 R>>endobj',
`% ${marker}`,
'trailer<</Root 1 0 R>>',
'%%EOF',
].join('\n')
return Buffer.from(pdf, 'latin1')
}
/**
* Prüft, dass keine generische 500-Seite ausgeliefert wurde.
* public/index.php zeigt bei unbehandelten Fehlern genau diesen Text —
* ein reiner Statuscode-Check würde ihn nicht zuverlässig erkennen.
*/
async function expectNoServerError(page) {
await expect(page.locator('body')).not.toContainText('Ein Fehler ist aufgetreten')
}
/**
* Öffnet einen Pfad und stellt sicher, dass er ohne Serverfehler rendert.
*
* Wichtig: Eine abgelaufene Session leitet auf /login um, was mit Status 200
* endet. Ohne die URL-Prüfung liefe jeder Test danach gegen die Login-Seite und
* schlüge mit einer irreführenden Meldung fehl.
*/
async function gotoOk(page, path) {
const response = await page.goto(path)
expect(response, `Keine Antwort für ${path}`).not.toBeNull()
expect(response.status(), `Unerwarteter Status für ${path}`).toBeLessThan(400)
expect(
new URL(page.url()).pathname,
`Statt ${path} kam die Login-Seite — die Session ist nicht (mehr) gültig.`
).not.toBe('/login')
await expectNoServerError(page)
return response
}
/** window.confirm() automatisch bestätigen (Löschdialoge, siehe app.js). */
function acceptDialogs(page) {
page.on('dialog', dialog => dialog.accept().catch(() => {}))
}
/**
* Lädt einen Beleg hoch und liefert dessen ID.
* Nach dem Upload landet man auf der Detailseite /belege/{id}.
*/
async function uploadBeleg(page, { marker, belegtyp = 'Eingangsrechnung', datum = null, rechnungsNr = null } = {}) {
const tag = marker || uniqueTag()
await gotoOk(page, '/belege/upload')
await page.setInputFiles('#beleg_file', {
name: `${tag}.pdf`,
mimeType: 'application/pdf',
buffer: pdfBuffer(tag),
})
await page.selectOption('#belegtyp', belegtyp)
if (datum) await page.fill('#beleg_datum', datum)
await page.fill('#rechnungs_nr', rechnungsNr || tag)
await page.fill('#notes', tag)
// Der Klick muss auf das Upload-Formular gescopet sein: In der Sidebar steht
// das Logout-Formular (layout/nav.php) und dessen Submit-Button käme im DOM
// zuerst — ein ungescopeter Treffer würde die Session beenden.
await Promise.all([
page.waitForURL(/\/belege\/\d+/),
page.click('form[action="/belege/upload"] button[type="submit"]'),
])
const id = belegIdFromUrl(page.url())
expect(id, `Beleg-ID nicht aus URL ermittelbar: ${page.url()}`).toBeGreaterThan(0)
return { id, tag }
}
function belegIdFromUrl(url) {
const m = url.match(/\/belege\/(\d+)/)
return m ? Number(m[1]) : 0
}
/**
* Räumt einen Beleg ab: erst dessen Buchungen (nur im Testmodus löschbar),
* dann den Beleg selbst. Fehler werden geschluckt — der Teardown darf einen
* bereits gescheiterten Test nicht zusätzlich rot färben.
*/
async function cleanupBeleg(page, belegId) {
if (!belegId) return
acceptDialogs(page)
try {
await page.goto(`/belege/${belegId}`)
// Buchungen des Belegs entfernen (Formulare mit action /bookings/{id}/delete).
for (;;) {
const form = page.locator('form[action^="/bookings/"][action$="/delete"]').first()
if (await form.count() === 0) break
await form.locator('button[type="submit"]').click()
await page.waitForLoadState('networkidle')
if (!page.url().includes(`/belege/${belegId}`)) {
await page.goto(`/belege/${belegId}`)
}
}
const delForm = page.locator(`form[action="/belege/${belegId}/delete"]`).first()
if (await delForm.count() > 0) {
await delForm.locator('button[type="submit"]').click()
await page.waitForLoadState('networkidle')
}
} catch {
// Aufräumen ist best effort.
}
}
/**
* Löscht eine Rechnung (nur im Testmodus erlaubt).
*
* Das Lösch-Formular steht in der Listenansicht (invoices/list.php), nicht in
* der Detailansicht — der Aufräumschritt muss deshalb über /invoices gehen.
*/
async function cleanupInvoice(page, invoiceId) {
if (!invoiceId) return
acceptDialogs(page)
try {
await page.goto('/invoices')
const form = page.locator(`form[action="/invoices/${invoiceId}/delete"]`).first()
if (await form.count() > 0) {
await form.locator('button[type="submit"]').click()
await page.waitForLoadState('networkidle')
}
} catch {
// Aufräumen ist best effort.
}
}
/** Betrag "1.234,56 €" → 1234.56 */
function parseMoney(text) {
if (!text) return NaN
const m = String(text).match(/-?[\d.]+,\d{2}/)
if (!m) return NaN
return Number(m[0].replace(/\./g, '').replace(',', '.'))
}
module.exports = {
TAG_PREFIX,
uniqueTag,
pdfBuffer,
expectNoServerError,
gotoOk,
acceptDialogs,
uploadBeleg,
belegIdFromUrl,
cleanupBeleg,
cleanupInvoice,
parseMoney,
}