// @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}/) }) })