Finanzmanager/tests/e2e/specs/bargeld.spec.js
fds2610-bot a5989eb262 Fix #136: Buchungen ohne Bargeld-Tool-Abgleich ermöglichen
Neuer Schalter „Ohne Bargeld-Abgleich“ in der Buchungsmaske für
Sammelbuchungen und Differenzausgleiche. Solche Buchungen werden nicht
gegen das Bargeld-Tool geprüft, ein früherer Status wird gelöscht, und sie
erscheinen nicht mehr unter den offenen Vorgängen im Cockpit. Kontoblatt
und Buchungsbericht zeigen einen leeren grauen Kreis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:58:17 +02:00

242 lines
10 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 { komboboxWaehlen } = require('./hilfen')
/*
* Der Abgleich mit dem Bargeld-Tool im Browser: die Kachel in der Verwaltung,
* das Formular dahinter und die Wirkung im Kontoblatt.
*
* Als Gegenstelle dient eine Adresse, die es nicht gibt. Das genügt: Der Abruf
* selbst steckt in der PHP-Suite, wo ein Testserver antwortet – hier geht es um
* den Weg und darum, dass ein gescheiterter Abruf die Zuordnung nicht aufhält.
*/
const BENUTZER = process.env.E2E_USER || 'e2e'
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
const MARKE = Date.now().toString().slice(-6)
const GIRO = `E2E Bargeld Giro ${MARKE}`
const KATEGORIE = `E2E Bargeld Haushalt ${MARKE}`
const ADRESSE = 'https://kasse.beispiel.invalid'
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(/\/$/)
}
/** Adresse und Kategorienauswahl zurücksetzen – die Instanz gehört nicht dem Test. */
async function zuruecksetzen(page) {
await page.goto('/verwaltung/bargeld')
await page.fill('#bargeld_url', '')
for (const feld of await page.locator('input[name="kategorien[]"]:checked').all()) {
await feld.uncheck()
}
await page.getByRole('button', { name: 'Speichern' }).click()
await expect(page.locator('.meldung')).toContainText('gespeichert')
}
test.describe('Bargeld-Tool', () => {
let giroId = null
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage()
await anmelden(page)
await page.goto('/konten/neu')
await page.fill('#name', GIRO)
await page.selectOption('#kontoart', 'bank')
await page.fill('#anfangssaldo', '1.000,00')
await page.click('.formular button[type="submit"]')
await expect(page).toHaveURL(/\/konten\/\d+\/bearbeiten/)
const treffer = page.url().match(/\/konten\/(\d+)\//)
giroId = treffer ? treffer[1] : null
await page.goto('/verwaltung/kategorien/neu')
await page.fill('#name', KATEGORIE)
await page.selectOption('#art', 'ausgabe')
await page.click('.formular button[type="submit"]')
await page.close()
})
test.beforeEach(async ({ page }) => {
await anmelden(page)
})
test.afterEach(async ({ page }) => {
await zuruecksetzen(page)
})
test('Die Verwaltung führt zur Konfiguration', async ({ page }) => {
await page.goto('/verwaltung')
const kachel = page.locator('.kachel', { hasText: 'Bargeld-Tool' })
await kachel.getByRole('link', { name: /einrichten/ }).click()
await expect(page).toHaveURL(/\/verwaltung\/bargeld$/)
await expect(page.locator('h1')).toContainText('Bargeld-Tool')
await expect(page.locator('#bargeld_url')).toBeVisible()
await expect(page.locator('#bargeld_benutzer')).toBeVisible()
await expect(page.locator('#bargeld_passwort')).toHaveAttribute('type', 'password')
await expect(page.getByRole('checkbox', { name: KATEGORIE })).not.toBeChecked()
})
test('Eine Adresse ohne Schema wird abgewiesen', async ({ page }) => {
await page.goto('/verwaltung/bargeld')
await page.fill('#bargeld_url', 'kasse.beispiel.invalid')
await page.getByRole('button', { name: 'Speichern' }).click()
await expect(page.locator('.meldung')).toContainText('http://')
await expect(page.locator('#bargeld_url')).toHaveValue('')
})
test('Der Verbindungstest meldet eine unerreichbare Gegenstelle', async ({ page }) => {
await page.goto('/verwaltung/bargeld')
await page.fill('#bargeld_url', ADRESSE)
await page.getByRole('button', { name: 'Speichern' }).click()
await expect(page.locator('.meldung')).toContainText('gespeichert')
await page.getByRole('button', { name: 'Verbindung testen' }).click()
await expect(page.locator('.meldung-fehler')).toContainText('nicht erreichbar')
})
test('Erst mit Adresse zeigt das Kontoblatt die Abgleichspalte', async ({ page }) => {
const empfaenger = `Ohne Bargeld ${MARKE}`
await page.goto(`/konten/${giroId}/buchung`)
await page.selectOption('#art', 'ausgabe')
await page.fill('#datum', '2026-04-03')
await page.fill('#betrag', '12,30')
await page.fill('#empfaenger', empfaenger)
await page.click('.buchungsmaske button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
// Ohne Adresse gibt es die Spalte nicht – sie zeigte nur graue Kreise.
await expect(page.locator('th.bargeldspalte')).toHaveCount(0)
await page.goto('/verwaltung/bargeld')
await page.fill('#bargeld_url', ADRESSE)
await page.fill('#bargeld_tage', '10')
await page.getByRole('button', { name: 'Speichern' }).click()
await expect(page.locator('.meldung')).toContainText('gespeichert')
await page.goto(`/konten/${giroId}`)
await expect(page.locator('th.bargeldspalte')).toHaveCount(1)
// Die Spalte steht zwischen Klasse und Betrag.
const spalten = await page.locator('table.kontoblatt thead th').allTextContents()
expect(spalten.indexOf('Bar')).toBe(spalten.indexOf('Klasse') + 1)
expect(spalten.indexOf('Betrag')).toBe(spalten.indexOf('Bar') + 1)
// Ohne vorgemerkte Kategorie steht dort der graue Kreis.
const zeile = page.locator('tr', { hasText: empfaenger })
await expect(zeile.locator('td.bargeldspalte .bargeld-neutral')).toBeVisible()
})
test('Eine vorgemerkte Kategorie wird beim Zuordnen geprüft', async ({ page }) => {
// Der Abruf läuft ins Leere; genau das soll das Ausrufezeichen zeigen –
// und die Zuordnung muss trotzdem stehen.
test.setTimeout(90_000)
const empfaenger = `Bargeldabhebung ${MARKE}`
await page.goto('/verwaltung/bargeld')
await page.fill('#bargeld_url', ADRESSE)
await page.getByRole('checkbox', { name: KATEGORIE }).check()
await page.getByRole('button', { name: 'Speichern' }).click()
await expect(page.locator('.meldung')).toContainText('gespeichert')
await page.goto(`/konten/${giroId}/buchung`)
await page.selectOption('#art', 'ausgabe')
await page.fill('#datum', '2026-04-04')
await page.fill('#betrag', '50,00')
await page.fill('#empfaenger', empfaenger)
await page.click('.buchungsmaske button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
const zeile = page.locator('tr', { hasText: empfaenger })
await zeile.locator('a.zuordnungsfeld').click()
await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), KATEGORIE)
// Die Zuordnung steht, der gescheiterte Abruf hat sie nicht aufgehalten.
// Über der Meldung steht noch die des Anlegens – gemeint ist die neue.
// Der Abruf selbst braucht bis zu BargeldTool::ZEITGRENZE (8 s) für die
// Verbindung – dazu kommt die Namensauflösung der ungültigen Adresse,
// zusammen bis über 15 s. Der Standard-Timeout von 5 s reicht dafür nicht.
await expect(page.locator('.meldung-erfolg').last())
.toContainText('Zuordnung gespeichert', { timeout: 25_000 })
await expect(page.locator('.meldung-erfolg').last())
.toContainText('fehlgeschlagen', { timeout: 25_000 })
const danach = page.locator('tr', { hasText: empfaenger })
await expect(danach).toContainText(KATEGORIE)
await expect(danach.locator('td.bargeldspalte .bargeld-fehler')).toBeVisible()
// Der offene Vorgang im Cockpit führt in den Buchungsbericht, gefiltert
// auf fehlgeschlagene Prüfungen – samt Kategorie (#131).
await page.goto('/')
await page.locator('.vorgangsliste a', { hasText: 'Bargeld-Tool-Prüfung fehlgeschlagen' }).click()
await expect(page).toHaveURL(/\/auswertungen\/bericht\?.*typ=buchungsbericht/)
await expect(page.locator('select[name="bargeld"]')).toHaveValue('fehler')
await expect(page.locator('table.buchungsgruppen thead')).toContainText('Kategorie')
const imBericht = page.locator('table.buchungsgruppen tr', { hasText: empfaenger })
await expect(imBericht).toContainText(KATEGORIE)
await expect(imBericht).toContainText(GIRO)
await expect(imBericht.locator('td.bargeldspalte .bargeld-fehler')).toBeVisible()
})
// Sammelbuchungen und Differenzausgleiche stehen absichtlich nicht im
// Bargeld-Tool (#136): Der Schalter nimmt sie aus dem Abgleich.
test('Ohne Bargeld-Abgleich wird nicht geprüft und nicht angemahnt', async ({ page }) => {
const empfaenger = `Sammelbuchung ${MARKE}`
// Ohne Adresse gibt es den Schalter nicht.
await page.goto(`/konten/${giroId}/buchung`)
await expect(page.locator('#ohne_bargeld_abgleich')).toHaveCount(0)
await page.goto('/verwaltung/bargeld')
await page.fill('#bargeld_url', ADRESSE)
await page.getByRole('checkbox', { name: KATEGORIE }).check()
await page.getByRole('button', { name: 'Speichern' }).click()
await expect(page.locator('.meldung')).toContainText('gespeichert')
await page.goto(`/konten/${giroId}/buchung`)
const schalter = page.locator('#ohne_bargeld_abgleich')
await expect(schalter).toBeVisible()
await expect(schalter).not.toBeChecked()
await page.selectOption('#art', 'ausgabe')
await page.fill('#datum', '2026-04-05')
await page.fill('#betrag', '73,20')
await page.fill('#empfaenger', empfaenger)
await page.selectOption('#zuordnung', { label: KATEGORIE })
await schalter.check()
await page.click('.buchungsmaske button[type="submit"]')
// Kein Abruf – also auch keine Meldung über einen gescheiterten.
await expect(page.locator('.meldung-erfolg')).toContainText('Buchung angelegt')
await expect(page.locator('.meldung-erfolg')).not.toContainText('Bargeld-Tool')
const zeile = page.locator('table.kontoblatt tbody tr', { hasText: empfaenger })
await expect(zeile).toContainText(KATEGORIE)
const marke = zeile.locator('td.bargeldspalte .bargeld-neutral')
await expect(marke).toHaveText('○')
await expect(marke).toHaveAttribute('title', 'Bewusst ohne Abgleich mit dem Bargeld-Tool')
// Beim Bearbeiten steht der Schalter wieder gesetzt da.
await zeile.getByRole('link', { name: empfaenger }).click()
await expect(page.locator('#ohne_bargeld_abgleich')).toBeChecked()
})
})