Finanzmanager/tests/e2e/specs/module.spec.js
fds2610-bot 78f1b91bc0 Online-Banking: Kontenzuordnung, Sammellöschen und drei Umsatz-Fehler
Kontenzuordnung als dritter Einrichtungsschritt: Welches Bankkonto gehört zu
welchem eigenen Konto? Umsätze merken sich über `bank_konto_id`, woher sie
stammen, und ziehen mit, wenn die Zuordnung erst nach einem Abruf fällt.
Abgerufene Umsätze buchen sich auf Wunsch selbst (`Services\Bankabgleich`).

Im Kontoblatt lassen sich Buchungen sammelweise löschen; Statusmeldungen liegen
jetzt über der Ansicht, statt den Inhalt springen zu lassen.

Dazu drei Fehler, die zusammen dafür sorgten, dass abgerufene Umsätze spurlos
verschwanden – der Abruf meldete sie als vorhanden, das Kontoblatt zeigte
nichts, und in der Vorschlagsliste standen sie ebenfalls nicht:

- Eine gelöschte Buchung ließ ihren Umsatz auf „uebernommen“ stehen. Der
  Fremdschlüssel setzte nur `buchung_id` auf NULL. `Buchung::loeschen()` gibt
  den Umsatz jetzt vorher frei, `Bankumsatz::verwaisteOeffnen()` fängt die Wege
  ab, die daran vorbeiführen – Kaskaden lösen in MariaDB keine Trigger aus.
- Ein Bankkonto entstand doppelt, wenn die Bank die IBAN nicht bei jedem Dialog
  mitliefert. Gesucht wird jetzt über die Kontonummer; eine nachgelieferte IBAN
  wird am vorhandenen Eintrag nachgetragen.
- Der Fingerabdruck hing an der IBAN und kippte mit ihr. Er nimmt jetzt die
  `bank_konto_id`, die fest steht, solange es den Bankzugang gibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:54:23 +02:00

194 lines
7.7 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('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 }) => {
await page.goto('/banking/neu')
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)
// 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('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')
})
})