// @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() }) })