Berichtstabellen nehmen nur noch den Platz, den ihre Spalten brauchen, und
stehen links; erst bei mehr Inhalt wachsen sie bis zur vollen Breite. Im
Scrollrahmen .breittabelle bleibt es beim bisherigen Verhalten, damit die
zwölf Monatsspalten weiter waagerecht scrollen statt umzubrechen.
Dabei kam heraus, dass die Zahlenspalten gar nicht rechtsbündig standen:
.rechts und .betrag sind nur einfach spezifisch, das weiter oben stehende
"table.liste th, table.liste td { text-align: left }" gewann. Für Berichte
steht die Ausrichtung jetzt ausdrücklich, sodass die letzte Spalte ihre
Beträge an der eigenen Kante ausrichtet.
Nachgezogen: Der E2E-Test der eingeklappten Seitenleiste erwartete
toBeHidden() für die Beschriftungen, die aber mit Absicht nur per clip-path
aus dem Bild geschoben werden – für Playwright bleiben sie sichtbar. Geprüft
wird jetzt, dass sie keinen Platz mehr einnehmen und der Verweis seinen
zugänglichen Namen behält.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
7.7 KiB
JavaScript
195 lines
7.7 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 tabelleBreit = (await tabelle.boundingBox()).width
|
||
const rahmenBreit = (await rahmen.boundingBox()).width
|
||
expect(tabelleBreit).toBeLessThan(rahmenBreit * 0.9)
|
||
|
||
// 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}/)
|
||
})
|
||
})
|