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.
182 lines
5.7 KiB
JavaScript
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,
|
|
}
|