Finanzmanager/tests/e2e/specs/module.spec.js
fds2610-bot a5e50f8529 Listen: Zahlenspalten überall rechtsbündig
Die Klasse .rechts wurde in Listen von der spezifischeren Grundregel
"table.liste th, table.liste td { text-align: left }" überstimmt – die
Ausrichtung war also überall wirkungslos, nicht nur in den Berichten. Die
Regel steht jetzt bei den Tabellen-Grundregeln und gilt für alle Listen:
Kontenübersicht, Kontoblatt, Wertpapiere, Depotbuchungen, Kategorien und
die Berichte. Die berichtseigene Sonderregel entfällt dadurch.

Budgetplaner und Splittabellen bringen ihre eigene Ausrichtung mit und
bleiben unberührt.

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

312 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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'
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(/\/$/)
}
/**
* Die Module aus den Phasen 4 bis 7: Cockpit-Kacheln, Online-Banking,
* Wertpapiere und die Nebenmodule.
*
* Geprüft wird, dass jeder Bereich erreichbar ist und seine Kernansicht
* aufbaut – nicht die Rechenlogik. Die steckt in der PHP-Suite, wo sie
* schneller und isoliert läuft.
*/
test.describe('Module der Phasen 4 bis 7', () => {
test.beforeEach(async ({ page }) => {
await anmelden(page)
})
test('Jeder Navigationspunkt führt in ein gebautes Modul', async ({ page }) => {
const punkte = [
['/konten', 'Kontenübersicht'],
['/banking', 'Bankverbindungen'],
['/zahlungsverkehr', 'Zahlungsverkehr'],
['/auswertungen', 'Auswertungen'],
['/planung', 'Planung'],
['/termine', 'Regelmäßige Buchungen'],
['/vertraege', 'Verträge'],
['/wertpapiere', 'Wertpapiere'],
['/verwaltung', 'Verwaltung'],
]
// Jeder Bereich hat sein Modul: Die Überschrift gehört zum Modul, nicht
// zur früheren Platzhalterseite.
for (const [pfad, titel] of punkte) {
await page.goto(pfad)
await expect(page.locator('h1')).toContainText(titel)
}
})
test('Zahlenspalten stehen in allen Listen rechtsbündig', async ({ page }) => {
// `.rechts` markiert Zahlen- und Aktionsspalten. Die Klasse muss die
// linksbündige Grundregel der Listen überall schlagen – nicht nur in den
// Berichten, wo es zuerst auffiel.
for (const pfad of ['/konten', '/wertpapiere', '/wertpapiere/buchungen']) {
await page.goto(pfad)
for (const art of ['th', 'td']) {
const zellen = page.locator(`table.liste ${art}.rechts`)
const anzahl = await zellen.count()
expect(anzahl, `${pfad}: ${art}.rechts vorhanden`).toBeGreaterThan(0)
for (const stelle of [0, anzahl - 1]) {
await expect(zellen.nth(stelle)).toHaveCSS('text-align', 'right')
}
}
}
})
test('Das Cockpit zeichnet seine Kacheln', async ({ page }) => {
await page.goto('/')
// Über die Überschriften, nicht über den Kacheltext: „Kontostände“ kommt
// auch im Hinweis der Vermögenskachel vor.
const ueberschriften = page.locator('.kachel h2')
for (const titel of [
'Vermögensentwicklung', 'Einnahmen und Ausgaben', 'Meine Ausgaben', 'Kontostände',
]) {
await expect(ueberschriften.filter({ hasText: titel })).toHaveCount(1)
}
})
test('Abgewählte Kacheln verschwinden und kommen zurück', async ({ page }) => {
await page.goto('/einstellungen?bereich=darstellung')
const kachel = page.locator('input[name="cockpit_kacheln[]"][value="ausgaben"]')
await kachel.uncheck()
await page.click('button:has-text("Speichern")')
await page.goto('/')
await expect(page.locator('.kachel h2', { hasText: 'Meine Ausgaben' })).toHaveCount(0)
await expect(page.locator('.kachelfuss')).toContainText('abgeschaltet')
await page.goto('/einstellungen?bereich=darstellung')
await page.locator('input[name="cockpit_kacheln[]"][value="ausgaben"]').check()
await page.click('button:has-text("Speichern")')
await page.goto('/')
await expect(page.locator('.kachel h2', { hasText: 'Meine Ausgaben' })).toHaveCount(1)
})
test('Eine Bankverbindung lässt sich anlegen und wieder löschen', async ({ page }) => {
// Vor den drei Einrichtungsschritten steht die Wahl der Art; die
// Bankenmaske hängt an der ersten der beiden Karten.
await page.goto('/banking/neu')
await page.click('.formatkarte:has-text("HBCI/FinTS")')
await page.fill('#name', 'E2E-Testbank')
await page.fill('#blz', '12030000')
await page.fill('#fints_url', 'https://banking.example.org/fints')
await page.fill('#benutzerkennung', 'e2e-kunde')
await page.fill('#pin', 'geheim')
// Schritt 1 speichert nur die Zugangsdaten.
await expect(page.locator('.schrittmarke')).toContainText('Schritt 1 von 3')
await page.click('button:has-text("Weiter")')
// Schritt 2 fragt die Bank selbst nach ihren TAN-Verfahren. Die Adresse
// im Test führt nirgendwohin – geprüft wird deshalb der Fehlerweg: Die
// Seite erklärt, was zu tun ist, statt stehen zu bleiben.
await expect(page.locator('.schrittmarke')).toContainText('Schritt 2 von 3')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
await expect(page.locator('.meldung-fehler')).toBeVisible()
await expect(page.locator('a:has-text("Zugangsdaten prüfen")')).toHaveCount(1)
// Wer die Einrichtung vorzeitig verlässt, landet in der Übersicht: Die
// Zugangsmaske böte nur noch an, was gerade durchlaufen wurde.
await page.click('a:has-text("Später einstellen")')
await expect(page.locator('h1')).toContainText('Bankverbindungen')
await expect(page.locator('td a:has-text("E2E-Testbank")')).toHaveCount(1)
await page.click('td a:has-text("E2E-Testbank")')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
// Die PIN wird nie wieder angezeigt.
await expect(page.locator('#pin')).toHaveValue('')
await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/)
// Ohne Wahl bleibt der Hinweis stehen, und die Seite ist wieder erreichbar.
await expect(page.locator('a:has-text("TAN-Verfahren einstellen")')).toHaveCount(1)
// Der Stichtag gibt es zweimal: als Vorgabe des Zugangs …
await page.selectOption('#abrufzeitraum', 'stichtag')
await page.fill('#abruf_stichtag', '2026-01-01')
await page.click('button:has-text("Speichern")')
await expect(page.locator('.meldung')).toContainText('gespeichert')
await expect(page.locator('#abruf_stichtag')).toHaveValue('2026-01-01')
// … und als einmalige Angabe neben dem Abrufknopf, die nichts speichert.
await expect(page.locator('.abrufform input[name="stichtag"]')).toHaveCount(1)
await page.goto('/banking')
await expect(page.locator('tr:has-text("E2E-Testbank")')).toContainText('Ab Stichtag')
await expect(page.locator('tr:has-text("E2E-Testbank")')).toContainText('01.01.2026')
await page.click('td a:has-text("E2E-Testbank")')
// Schritt 3: die Kontenzuordnung. Ohne erreichbare Bank kennt sie noch
// kein Konto und bietet die Abfrage an, statt eine leere Tabelle zu zeigen.
await page.click('a:has-text("Konten zuordnen")')
await expect(page.locator('h1')).toContainText('Konten zuordnen')
await expect(page.locator('.platzhalter')).toContainText('Noch kein Konto bekannt')
await expect(page.locator('button:has-text("Konten bei der Bank abfragen")')).toHaveCount(1)
await page.click('a:has-text("Zurück zur Bankverbindung")')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
page.once('dialog', dialog => dialog.accept())
await page.click('button:has-text("Bankverbindung löschen")')
await expect(page.locator('.meldung')).toContainText('gelöscht')
})
test('Eine falsche IBAN wird beim Anlegen einer Adresse abgewiesen', async ({ page }) => {
await page.goto('/vertraege/adressen')
await page.fill('#name', 'E2E-Falschtest')
await page.fill('#iban', 'DE02120300000000202052')
await page.click('button:has-text("Adresse anlegen")')
await expect(page.locator('.meldung-fehler')).toContainText('IBAN')
})
test('Ein Kredit zeigt seinen gerechneten Tilgungsplan', async ({ page }) => {
await page.goto('/vertraege/kredite')
await page.fill('#name', 'E2E-Darlehen')
await page.fill('#darlehensbetrag', '100000')
await page.fill('#zinssatz', '3,0')
await page.fill('#rate', '500')
await page.click('button:has-text("Kredit anlegen")')
await expect(page.locator('h3', { hasText: 'Tilgungsplan' })).toContainText('E2E-Darlehen')
await expect(page.locator('.kennzahltitel', { hasText: 'Zinsen gesamt' })).toHaveCount(1)
// Erste Rate: 250 Zinsen, 250 Tilgung. Die erste Tabelle ist die
// Kreditliste, der Plan steht in der zweiten.
const ersteRate = page.locator('table.liste').last().locator('tbody tr').first()
await expect(ersteRate).toContainText('250,00')
page.once('dialog', dialog => dialog.accept())
await page.locator('tr', { hasText: 'E2E-Darlehen' }).locator('button:has-text("Löschen")').first().click()
await expect(page.locator('.meldung')).toContainText('gelöscht')
})
test('Depotbuchungen lassen sich über die Auswahl sammelweise löschen', async ({ page }) => {
const marke = Date.now().toString().slice(-6)
const depot = `E2E Depot ${marke}`
const papier = `E2E Papier ${marke}`
await page.goto('/konten/neu')
await page.fill('#name', depot)
await page.selectOption('#kontoart', 'depot')
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
// Zwei Käufe desselben Papiers – der zweite über die Auswahlliste, damit
// kein zweites Wertpapier entsteht.
for (const [nr, stueck] of [[1, '10'], [2, '5']]) {
await page.goto('/wertpapiere/buchungen/neu')
await page.selectOption('#konto_id', { label: depot })
await page.fill('#datum', '2026-05-0' + nr)
if (nr === 1) {
await page.fill('#wertpapier_name', papier)
} else {
await page.selectOption('#wertpapier_id', { label: papier })
}
await page.fill('#stueck', stueck)
await page.fill('#kurs', '100')
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
}
// Der Filter bleibt über das Löschen hinweg stehen – das Formularziel
// trägt ihn mit.
await page.goto('/wertpapiere/buchungen')
await page.selectOption('select[name="depot"]', { label: depot })
await page.click('button:has-text("Filtern")')
await expect(page.locator('tbody tr')).toHaveCount(2)
// Der Bestand kennt beide Käufe.
await page.goto('/wertpapiere')
await page.selectOption('select[name="depot"]', { label: depot })
await page.click('button:has-text("Anzeigen")')
await expect(page.locator('tbody tr', { hasText: papier })).toContainText('15')
await page.goBack()
await page.goBack()
// Ohne Auswahl bleibt der Knopf gesperrt.
const knopf = page.locator('#depotbuchungsauswahl button')
await expect(knopf).toBeDisabled()
await page.check('.alle-waehlen')
await expect(knopf).toContainText('(2)')
const gefragt = []
page.on('dialog', async (dialog) => {
gefragt.push(dialog.message())
await dialog.accept()
})
await knopf.click()
expect(gefragt.join(' ')).toContain('löschen')
await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht')
await expect(page).toHaveURL(/depot=/)
// Der Name steht weiter im Filterfeld – gemeint ist die Liste.
await expect(page.locator('table.liste')).toHaveCount(0)
await expect(page.locator('.platzhalter')).toContainText('Keine Depotbuchungen')
// Und der Bestand ist die Buchungen los – er wird gerechnet.
await page.goto('/wertpapiere')
await page.selectOption('select[name="depot"]', { label: depot })
await page.click('button:has-text("Anzeigen")')
await expect(page.locator('.platzhalter')).toContainText('Keine Bestände')
// Aufräumen: Das Testdepot wieder entfernen. Die Rückfrage übernimmt der
// Dialoghandler von oben.
await page.goto('/konten')
const pfad = await page.locator('a', { hasText: depot }).first().getAttribute('href')
await page.goto(pfad + '/bearbeiten')
await page.locator('button:has-text("Konto löschen")').click()
await expect(page.locator('.meldung')).toContainText('gelöscht')
})
test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => {
await page.goto('/termine/kalender')
await expect(page.locator('h1')).toContainText('Finanzkalender')
await page.selectOption('#tage', '90')
await page.click('button:has-text("Anzeigen")')
await expect(page).toHaveURL(/tage=90/)
})
test('Der Zahlungsverkehr weist eine falsche IBAN ab', async ({ page }) => {
await page.goto('/zahlungsverkehr/neu')
await page.fill('#empfaenger', 'E2E-Empfänger')
await page.fill('#iban', 'DE00000000000000000000')
await page.fill('#betrag', '10,00')
await page.click('button:has-text("Erfassen")')
// Die Bankverbindung ist bewusst nicht Pflicht: Ein Auftrag darf erfasst
// werden, bevor eine eingerichtet ist. Geprüft wird sie erst beim Senden.
await expect(page.locator('.meldung-fehler')).toContainText('IBAN')
})
})