Nach einem Import, der schiefging, war das Löschen Zeile für Zeile mühsam. Die Liste der Depotbuchungen räumt jetzt so auf wie das Kontoblatt: Ankreuzfeld je Zeile, das Feld in der Titelzeile wählt alle angezeigten, „Auswahl löschen“ in der Aktionsleiste schickt sie an POST /wertpapiere/buchungen/loeschen. Der Knopf nennt die Anzahl und bleibt ohne Auswahl gesperrt; die Rückfrage nennt sie ebenfalls. Ohne JavaScript funktioniert alles weiter – dann ist der Knopf offen und der Controller weist eine leere Auswahl ab. Mit jeder Depotbuchung geht ihre Geldbuchung, wie beim Löschen einer einzelnen. Am Bestand ist nichts nachzuziehen: Er wird bei jedem Aufruf aus den Depotbuchungen gerechnet und gibt die Löschung von selbst wieder her, ebenso realisierter Gewinn, Veräußerungen und Erträge. Der eingestellte Filter (Depot, Wertpapier, Vorgang) hängt am Ziel des Formulars, deshalb steht danach wieder dieselbe Ansicht da. Zusammengesetzt wird er im Controller neu, damit kein fremdes Ziel eingeschleust werden kann. Das gilt jetzt auch für das „Löschen“ je Zeile, das bisher zur ungefilterten Liste zurücksprang. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
275 lines
11 KiB
JavaScript
275 lines
11 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'
|
||
|
||
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('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')
|
||
})
|
||
})
|