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>
201 lines
8 KiB
JavaScript
201 lines
8 KiB
JavaScript
// @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}/)
|
||
})
|
||
})
|