Finanzmanager/tests/e2e/specs/auswertungen.spec.js
fds2610-bot 6ca247a40d Berichte: Tabelle mittig statt links ausgerichtet
Linksbündig wirkte die schmale Tabelle verloren neben dem zentrierten
Berichtskopf. Sie steht jetzt mittig unter Überschrift und Zeitraumzeile;
die Zahlenspalten bleiben in sich rechtsbündig. Der E2E-Test prüft, dass
links und rechts gleich viel Luft bleibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:04:19 +02:00

201 lines
8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

// @ts-check
const { test, expect } = require('@playwright/test')
const BENUTZER = process.env.E2E_USER || 'e2e'
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
// Eindeutige Namen je Lauf: die Tests arbeiten auf einer bestehenden Instanz
// und dürfen sich nicht an Resten eines früheren Laufs stoßen.
const MARKE = Date.now().toString().slice(-6)
async function anmelden(page) {
await page.goto('/login')
if (page.url().includes('/einrichtung')) {
throw new Error(
'Instanz ist noch nicht eingerichtet. Zuerst einen Benutzer anlegen: ' +
'docker compose exec -u www-data php php scripts/e2e-benutzer.php'
)
}
await page.fill('#benutzername', BENUTZER)
await page.fill('#passwort', PASSWORT)
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/$/)
}
/** Öffnet einen ungespeicherten Bericht des gewünschten Typs. */
async function vorlage(page, typ, weitere = {}) {
const parameter = new URLSearchParams({
typ, zeitraum: 'aktuelles_jahr', angepasst: '1', ...weitere,
})
await page.goto(`/auswertungen/bericht?${parameter}`)
}
test.describe('Auswertungen', () => {
test.beforeEach(async ({ page }) => {
await anmelden(page)
})
test('Übersicht führt über die Standardvorlagen in den Summenbericht', async ({ page }) => {
await page.click('.seitenleiste a[href="/auswertungen"]')
await expect(page.locator('h1')).toContainText('Auswertungen')
// Alle fünf Standardvorlagen stehen bereit.
const vorlagen = page.locator('.kachel', { hasText: 'Standardvorlagen' }).locator('.berichtsliste li')
await expect(vorlagen).toHaveCount(5)
await page.click('.berichtsliste a:has-text("Summenbericht")')
await expect(page.locator('.berichtskopf h2')).toContainText('Summenbericht')
await expect(page.locator('.berichtskopf p')).toContainText('in Euro')
})
test('Die fünf Berichtstypen zeigen jeweils ihre eigene Tabelle', async ({ page }) => {
await vorlage(page, 'summenbericht')
await expect(page.locator('thead th').first()).toContainText('Kategoriebeschreibung')
await vorlage(page, 'buchungsbericht')
await expect(page.locator('thead th').first()).toContainText('Datum')
await vorlage(page, 'vergleichsbericht')
await expect(page.locator('thead th', { hasText: 'Veränderung' })).toHaveCount(1)
await vorlage(page, 'kontostandsbericht')
await expect(page.locator('thead th').nth(1)).toContainText('Kontengruppe')
await vorlage(page, 'budgetbericht')
await expect(page.locator('thead th', { hasText: 'Saldo' })).toHaveCount(1)
})
test('Spalten-Aufteilung macht aus einer Spalte zwölf Monate', async ({ page }) => {
await vorlage(page, 'summenbericht')
await expect(page.locator('table.bericht thead th')).toHaveCount(2)
await page.selectOption('select[name="aufteilung"]', 'monat')
await page.click('button:has-text("Anzeigen")')
// Bezeichnung, zwölf Monate, Summe.
await expect(page.locator('table.bericht thead th')).toHaveCount(14)
await expect(page.locator('thead th', { hasText: 'Januar' })).toHaveCount(1)
})
test('Zeilen-Summe wechselt die Dimension der Zeilen', async ({ page }) => {
await vorlage(page, 'summenbericht', { zeilensumme: 'empfaenger' })
await expect(page.locator('table.bericht thead th').first()).toContainText('Bezeichnung')
// Beim Kontostandsbericht ist die Wahl festgelegt und das Feld gesperrt.
await vorlage(page, 'kontostandsbericht')
await expect(page.locator('select[name="zeilensumme"]')).toBeDisabled()
})
test('Ein Klick auf eine Zahl führt in die Buchungen dahinter und zurück', async ({ page }) => {
await vorlage(page, 'summenbericht')
const zelle = page.locator('table.bericht .wertlink').first()
const betrag = (await zelle.innerText()).trim()
await zelle.click()
await expect(page.locator('h1')).not.toBeEmpty()
await expect(page.locator('.berichtskopf p')).toContainText('Kategorie')
// Die Summe des Drilldowns ist derselbe Betrag – im Bericht ohne
// Vorzeichen, hier mit.
await expect(page.locator('tfoot .summenzeile')).toContainText(betrag.replace('-', ''))
await page.click('a:has-text("Zurück zum Bericht")')
await expect(page.locator('.berichtskopf h2')).toContainText('Summenbericht')
})
test('Bericht speichern, in der Übersicht wiederfinden und löschen', async ({ page }) => {
page.on('dialog', d => d.accept())
const name = `E2E-Bericht ${MARKE}`
const gruppe = `E2E-Gruppe ${MARKE}`
await vorlage(page, 'summenbericht', { zeitraum: 'letzter_monat', aufteilung: 'monat' })
await page.click('summary:has-text("Speichern unter")')
await page.fill('input[name="name"]', name)
await page.fill('input[name="neue_gruppe"]', gruppe)
await page.click('button:has-text("Als neuen Bericht speichern")')
await expect(page.locator('.meldung-erfolg')).toContainText('gespeichert')
await expect(page).toHaveURL(/\/auswertungen\/bericht\/\d+/)
// Der gespeicherte Filter kommt beim erneuten Öffnen zurück.
await expect(page.locator('select[name="zeitraum"]')).toHaveValue('letzter_monat')
await expect(page.locator('select[name="aufteilung"]')).toHaveValue('monat')
// In der Übersicht steht er in seiner eigenen Gruppenkachel.
await page.goto('/auswertungen')
const kachel = page.locator('.kachel', { hasText: gruppe })
await expect(kachel.locator('.berichtsliste a', { hasText: name })).toHaveCount(1)
await kachel.locator('.berichtsliste a', { hasText: name }).click()
await page.click('button:has-text("Löschen")')
await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht')
await expect(page.locator('.berichtsliste a', { hasText: name })).toHaveCount(0)
// Die Gruppe aufräumen; ihre Berichte sind bereits weg. Ihr Name steht
// dort in einem Eingabefeld, nicht als Text – deshalb über den Wert
// suchen und nicht über hasText.
await page.goto('/auswertungen/gruppen')
const zeile = page.locator('tr').filter({ has: page.locator(`input[value="${gruppe}"]`) })
await zeile.locator('button:has-text("Löschen")').click()
await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht')
})
test('Schmale Berichte bleiben schmal, die Zahlenspalte bleibt rechtsbündig', async ({ page }) => {
// Zwei Spalten brauchen keine Bildschirmbreite: Die Tabelle steht links
// und ist deutlich schmaler als der Platz, den sie hätte.
await vorlage(page, 'summenbericht')
const tabelle = page.locator('table.bericht')
const rahmen = page.locator('.breittabelle')
const tabelleKasten = await tabelle.boundingBox()
const rahmenKasten = await rahmen.boundingBox()
const tabelleBreit = tabelleKasten.width
expect(tabelleBreit).toBeLessThan(rahmenKasten.width * 0.9)
// … und steht dabei mittig: links und rechts bleibt gleich viel Luft.
const links = tabelleKasten.x - rahmenKasten.x
const rechts = (rahmenKasten.x + rahmenKasten.width) - (tabelleKasten.x + tabelleBreit)
expect(Math.abs(links - rechts)).toBeLessThanOrEqual(1)
// Letzte Spalte: Zahlen rechtsbündig in ihrer eigenen Spalte.
// Die erste Zeile eines Abschnitts ist seine Überschrift (th) – deshalb
// die erste echte Wertzeile.
const letzte = tabelle.locator('tbody tr > td:last-child').first()
await expect(letzte).toHaveCSS('text-align', 'right')
// Zwölf Monatsspalten dürfen dagegen weiterhin über die Breite gehen
// und im Rahmen scrollen, statt umzubrechen.
await page.selectOption('select[name="aufteilung"]', 'monat')
await page.click('button:has-text("Anzeigen")')
expect((await tabelle.boundingBox()).width)
.toBeGreaterThan(tabelleBreit)
})
test('CSV-Ausgabe kommt als Datei mit deutschem Zahlenformat', async ({ page }) => {
await vorlage(page, 'summenbericht')
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('a:has-text("Als CSV")'),
])
expect(download.suggestedFilename()).toMatch(/\.csv$/)
const strom = await download.createReadStream()
const stuecke = []
for await (const stueck of strom) {
stuecke.push(stueck)
}
const inhalt = Buffer.concat(stuecke).toString('utf8')
expect(inhalt.startsWith('')).toBe(true)
expect(inhalt).toContain('Bezeichnung;')
expect(inhalt).toMatch(/;\d+,\d{2}/)
})
})