Merge pull request 'Fix #62: E2E-Tests an den Typeahead im Kontoblatt angepasst' (#63) from fds2610-bot/Finanzmanager:main into main

Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/63
This commit is contained in:
fds2610 2026-09-04 15:35:00 +02:00
commit f4fa4f0e3a
7 changed files with 99 additions and 20 deletions

View file

@ -1,5 +1,33 @@
# Änderungen
## E2E-Tests an den Typeahead im Kontoblatt angepasst (04.09.2026)
Zwölf E2E-Tests standen seit dem Typeahead-Umbau (Fix #41) auf Rot – nicht, weil die
Anwendung etwas falsch machte, sondern weil die Specs den alten Stand prüften. Vier
Ursachen lagen darunter:
- **Die Kombobox versteckt ihr `<select>`.** `kombobox.js` lässt das ursprüngliche Feld
zwar im Formular stehen, setzt es aber auf `hidden`; `selectOption()` wartet darauf
vergeblich, bis der Test nach 30 Sekunden abbricht. Der neue Helfer
`tests/e2e/specs/hilfen.js` bedient stattdessen das Eingabefeld: Er tippt die
Beschriftung ein – das ist zugleich die Probe auf die Filterung – und klickt den genau
passenden Treffer an. Exakt muss der Vergleich sein, weil `[Depot]` sonst auch das
`[Depot] …` des Wertpapiergeschäfts träfe.
- **Zwei Erfolgsmeldungen übereinander.** Seit dem Schnellspeichern ohne Neuladen
(Fix #40) bleibt die Meldung des Anlegens stehen und die neue kommt darunter. Die Tests
meinen die neue und greifen jetzt zu `.last()`.
- **Die Adressliste ist eine Bearbeitungstabelle.** Seit Fix #51 stehen die Namen in
Eingabefeldern, wo `toContainText` nicht hinsieht. Geprüft wird nun der Feldwert – die
Adresse selbst wurde die ganze Zeit korrekt gespeichert.
- **Zwei Folgefehler.** Playwright startet nach jedem Fehlschlag einen frischen Worker;
damit wird das Spec-Modul neu geladen, seine Marke neu gezogen und `beforeAll` legt neue,
leere Konten an. Die Tests „Kontoblatt lässt sich durchsuchen“ und „Löschen fragt nach“
suchten deshalb eine Buchung, die auf dem Konto des vorigen Workers lag. Sie brauchten
keine Änderung.
Die Suite läuft wieder vollständig durch: 87 Tests grün, und ohne die Timeouts in 2,7
statt 7,1 Minuten.
## FinTS-Bibliothek auf den aktuellen Upstream-Stand gehoben (04.09.2026)
Der Fork `nemiah/php-fints` steht jetzt auf `v4.1.4` und damit auf dem Upstream-Stand vom

View file

@ -51,6 +51,20 @@ Hierher gehört nur, was einen Browser braucht: Formularabläufe, Redirects,
gerenderte Views, JavaScript. Rechen- und Model-Logik gehört in die PHP-Suite
(`tests/NN_*.php`) – schneller und isoliert.
## Gemeinsame Helfer
`specs/hilfen.js` liegt neben den Specs, endet aber nicht auf `.spec.js` und wird
deshalb nicht als Testdatei eingesammelt. Dort steht `komboboxWaehlen()`: Die
Typeahead-Felder aus `public/static/kombobox.js` lassen ihr `<select>` versteckt
im Formular stehen, `selectOption()` läuft darauf in einen Timeout. Der Helfer
tippt stattdessen die Beschriftung ein und klickt den genau passenden Treffer an.
```js
const { komboboxWaehlen } = require('./hilfen')
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), 'Lebensmittel')
```
## Berichte
HTML-Report, Screenshots und Traces landen nach einem Lauf in

View file

@ -1,5 +1,6 @@
// @ts-check
const { test, expect } = require('@playwright/test')
const { komboboxWaehlen } = require('./hilfen')
/*
* Der Abgleich mit dem Bargeld-Tool im Browser: die Kachel in der Verwaltung,
@ -167,11 +168,12 @@ test.describe('Bargeld-Tool', () => {
const zeile = page.locator('tr', { hasText: empfaenger })
await zeile.locator('a.zuordnungsfeld').click()
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: KATEGORIE })
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), KATEGORIE)
// Die Zuordnung steht, der gescheiterte Abruf hat sie nicht aufgehalten.
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung gespeichert')
await expect(page.locator('.meldung-erfolg')).toContainText('fehlgeschlagen')
// Über der Meldung steht noch die des Anlegens – gemeint ist die neue.
await expect(page.locator('.meldung-erfolg').last()).toContainText('Zuordnung gespeichert')
await expect(page.locator('.meldung-erfolg').last()).toContainText('fehlgeschlagen')
const danach = page.locator('tr', { hasText: empfaenger })
await expect(danach).toContainText(KATEGORIE)

View file

@ -1,5 +1,6 @@
// @ts-check
const { test, expect } = require('@playwright/test')
const { komboboxWaehlen } = require('./hilfen')
const BENUTZER = process.env.E2E_USER || 'e2e'
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
@ -172,11 +173,15 @@ test.describe('Buchungen', () => {
await zeile.locator('a.zuordnungsfeld').click()
// Die Auswahlliste gibt es einmal und wandert in die angeklickte Zelle.
const liste = zeile.locator('#zuordnungsauswahl select')
// Sichtbar wird das Formular; das <select> darin bleibt hinter der
// Kombobox verborgen.
const liste = zeile.locator('#zuordnungsauswahl')
await expect(liste).toBeVisible()
await liste.selectOption({ label: KATEGORIE })
await komboboxWaehlen(liste.locator('select'), KATEGORIE)
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung')
// Die Meldung des Anlegens steht noch da: Das Schnellspeichern lädt
// die Seite nicht neu, sondern hängt seine eigene darunter.
await expect(page.locator('.meldung-erfolg').last()).toContainText('Zuordnung')
await expect(page.locator('tr', { hasText: `Ohne Kategorie ${MARKE}` }))
.toContainText(KATEGORIE)
})
@ -202,9 +207,9 @@ test.describe('Buchungen', () => {
const zeile = page.locator('tr', { hasText: `Kein Neuladen ${MARKE}` })
await zeile.locator('a.zuordnungsfeld').click()
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: KATEGORIE })
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), KATEGORIE)
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung')
await expect(page.locator('.meldung-erfolg').last()).toContainText('Zuordnung')
await expect(zeile).toContainText(KATEGORIE)
// Ein echtes Neuladen hätte sowohl die Markierung gelöscht als auch die
@ -229,7 +234,7 @@ test.describe('Buchungen', () => {
await expect(liste).toBeVisible()
await liste.selectOption({ label: KLASSE })
await expect(page.locator('.meldung-erfolg')).toContainText('Klasse gespeichert')
await expect(page.locator('.meldung-erfolg').last()).toContainText('Klasse gespeichert')
await expect(page.locator('tr', { hasText: `Ohne Klasse ${MARKE}` }))
.toContainText(KLASSE)
@ -238,7 +243,7 @@ test.describe('Buchungen', () => {
await danach.locator('a.klassenfeld').click()
await danach.locator('#klassenauswahl select').selectOption('')
await expect(page.locator('.meldung-erfolg')).toContainText('Klasse gelöscht')
await expect(page.locator('.meldung-erfolg').last()).toContainText('Klasse gelöscht')
await expect(page.locator('tr', { hasText: `Ohne Klasse ${MARKE}` }))
.not.toContainText(KLASSE)
})
@ -253,9 +258,9 @@ test.describe('Buchungen', () => {
const zeile = page.locator('tr', { hasText: `Nachträglich ${MARKE}` })
await zeile.locator('a.zuordnungsfeld').click()
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: `[${SPAR}]` })
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), `[${SPAR}]`)
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung')
await expect(page.locator('.meldung-erfolg').last()).toContainText('Zuordnung')
await expect(page.locator('tr', { hasText: `Nachträglich ${MARKE}` }))
.toContainText(`[${SPAR}]`)
@ -275,7 +280,7 @@ test.describe('Buchungen', () => {
const zeile = page.locator('tr', { hasText: `Aufzuteilen ${MARKE}` })
await zeile.locator('a.zuordnungsfeld').click()
await zeile.locator('#zuordnungsauswahl select').selectOption('split')
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), 'Splitbuchung …')
const dialog = page.locator('#splitdialog')
await expect(dialog).toBeVisible()
@ -292,7 +297,7 @@ test.describe('Buchungen', () => {
// Der Dialog hat zwei Submit-Knöpfe: Speichern und Löschen.
await dialog.locator('button[type="submit"].primaer').click()
await expect(page.locator('.meldung-erfolg')).toContainText('Splitbuchung')
await expect(page.locator('.meldung-erfolg').last()).toContainText('Splitbuchung')
await expect(page.locator('tr', { hasText: `Aufzuteilen ${MARKE}` }).first())
.toContainText('Splitbuchung')
@ -320,19 +325,19 @@ test.describe('Buchungen', () => {
const dialog = page.locator('#splitdialog')
await expect(dialog).toBeVisible()
await expect(page.locator('#zuordnungsauswahl select')).toBeHidden()
await expect(page.locator('#zuordnungsauswahl')).toBeHidden()
await expect(dialog.locator('input[name="split[0][betrag]"]')).toHaveValue('-30,00')
await dialog.locator('#splitloeschen').click()
await expect(page.locator('.meldung-erfolg')).toContainText('Splitbuchung gelöscht')
await expect(page.locator('.meldung-erfolg').last()).toContainText('Splitbuchung gelöscht')
const danach = page.locator('tr', { hasText: `Wieder einfach ${MARKE}` })
await expect(danach).not.toContainText('Splitbuchung')
await expect(danach).not.toContainText(KATEGORIE)
// Und die Zuordnung geht jetzt wieder über die Auswahlliste.
await danach.locator('a.zuordnungsfeld').click()
await expect(danach.locator('#zuordnungsauswahl select')).toBeVisible()
await expect(danach.locator('#zuordnungsauswahl')).toBeVisible()
})
test('Splitbuchung mit zwei Zeilen', async ({ page }) => {

26
tests/e2e/specs/hilfen.js Normal file
View file

@ -0,0 +1,26 @@
// @ts-check
/* Gemeinsame Helfer der E2E-Specs. Die Datei endet nicht auf `.spec.js` und
* wird deshalb nicht als Testdatei eingesammelt. */
/**
* Wählt in einer Typeahead-Kombobox (public/static/kombobox.js) den Eintrag
* mit genau dieser Beschriftung.
*
* Seit dem Umbau steht das ursprüngliche `<select>` nur noch versteckt im
* Formular; bedient wird das Eingabefeld daneben. `selectOption()` läuft
* darauf in einen Timeout. Der Helfer tippt die Beschriftung ein – das ist
* zugleich die Probe auf die Filterung – und klickt den Treffer an.
*
* @param {import('@playwright/test').Locator} select das versteckte <select>
* @param {string} beschriftung Text des Eintrags, exakt
*/
async function komboboxWaehlen(select, beschriftung) {
const kombobox = select.locator('xpath=ancestor::div[contains(@class,"kombobox")][1]')
await kombobox.locator('.kombobox-eingabe').fill(beschriftung)
// Exakt, sonst träfe „[Depot]“ auch das „[Depot] …“ des Wertpapiergeschäfts.
await kombobox.getByRole('option', { name: beschriftung, exact: true }).click()
}
module.exports = { komboboxWaehlen }

View file

@ -323,8 +323,11 @@ test.describe('Module der Phasen 4 bis 7', () => {
await expect(page.locator('.meldung-erfolg')).toContainText('erfasst')
// Die Adresse landet wirklich im Adressbuch – nicht nur der Auftrag.
// Die Liste ist eine Bearbeitungstabelle: Die Namen stehen in
// Eingabefeldern, nicht als Text.
await page.goto('/vertraege/adressen')
await expect(page.locator('table.liste')).toContainText(empfaenger)
await expect(page.locator(`table.liste input[name="name"][value="${empfaenger}"]`))
.toHaveCount(1)
})
test('Die Empfängerauswahl im Zahlungsverkehr ist ein Typeahead aus dem Adressbuch', async ({ page }) => {

View file

@ -1,5 +1,6 @@
// @ts-check
const { test, expect } = require('@playwright/test')
const { komboboxWaehlen } = require('./hilfen')
/*
* Der Weg, den die Wertpapierabrechnung nimmt: Sie kommt als Buchung aufs
@ -107,7 +108,7 @@ test.describe('Wertpapiergeschäft aus dem Kontoblatt', () => {
const zeile = page.locator('tr', { hasText: empfaenger })
await zeile.locator('a.zuordnungsfeld').click()
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: `[${DEPOT}] …` })
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), `[${DEPOT}] …`)
const dialog = page.locator('#wertpapierdialog')
await expect(dialog).toBeVisible()
@ -166,7 +167,7 @@ test.describe('Wertpapiergeschäft aus dem Kontoblatt', () => {
const zeile = page.locator('tr', { hasText: empfaenger })
await zeile.locator('a.zuordnungsfeld').click()
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: `[${DEPOT}] …` })
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), `[${DEPOT}] …`)
await expect(page.locator('#wertpapierdialog')).toBeVisible()
await page.fill('#wp_name', `Sparplan-Fonds ${MARKE}`)