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>
312 lines
12 KiB
JavaScript
312 lines
12 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('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')
|
||
})
|
||
})
|