Die Klassenspalte ist anklickbar wie die Zuordnung daneben: Ein Klick
öffnet die Klassenliste in der Zelle, die Auswahl wird sofort über
POST /buchungen/{id}/klasse gespeichert. Buchung::klasseSetzen() lässt
Kategorie, Gegenkonto und Splitzeilen unberührt.
Die Buchungszeilen sind jetzt alle zwei Textzeilen hoch, ihr Inhalt steht
senkrecht in der Mitte. Was nicht auf zwei Zeilen passt, wird
abgeschnitten; den vollständigen Text zeigt der Tooltip – bei Empfänger
und Buchungstext nur dort, wo wirklich etwas fehlt, über dem
Kategoriefeld einer Splitbuchung deren Zeilen mit Kategorie und Betrag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
447 lines
18 KiB
JavaScript
447 lines
18 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'
|
||
|
||
const MARKE = Date.now().toString().slice(-6)
|
||
const GIRO = `E2E Giro ${MARKE}`
|
||
const SPAR = `E2E Spar ${MARKE}`
|
||
const KATEGORIE = `E2E Einkauf ${MARKE}`
|
||
const KLASSE = `E2E Privat ${MARKE}`
|
||
|
||
async function anmelden(page) {
|
||
await page.goto('/login')
|
||
if (page.url().includes('/einrichtung')) {
|
||
throw new Error(
|
||
'Instanz ist noch nicht eingerichtet: ' +
|
||
'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(/\/$/)
|
||
}
|
||
|
||
/** Legt ein Konto an und liefert dessen Kennung aus der URL. */
|
||
async function kontoAnlegen(page, name, anfangssaldo) {
|
||
await page.goto('/konten/neu')
|
||
await page.fill('#name', name)
|
||
await page.fill('#anfangssaldo', anfangssaldo)
|
||
await page.click('.formular button[type="submit"]')
|
||
await expect(page).toHaveURL(/\/konten\/\d+\/bearbeiten/)
|
||
|
||
const treffer = page.url().match(/\/konten\/(\d+)\//)
|
||
return treffer ? treffer[1] : null
|
||
}
|
||
|
||
test.describe('Buchungen', () => {
|
||
let giroId = null
|
||
let sparId = null
|
||
|
||
test.beforeAll(async ({ browser }) => {
|
||
const page = await browser.newPage()
|
||
await anmelden(page)
|
||
|
||
giroId = await kontoAnlegen(page, GIRO, '1.000,00')
|
||
sparId = await kontoAnlegen(page, SPAR, '5.000,00')
|
||
|
||
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.goto('/verwaltung/klassen')
|
||
await page.fill('form[action="/verwaltung/klassen/neu"] #name', KLASSE)
|
||
await page.click('form[action="/verwaltung/klassen/neu"] button[type="submit"]')
|
||
|
||
await page.close()
|
||
})
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
await anmelden(page)
|
||
})
|
||
|
||
test('Kontoblatt zeigt den Anfangssaldo', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}`)
|
||
|
||
await expect(page.locator('h1')).toContainText(GIRO)
|
||
await expect(page.locator('.saldoanzeige').first()).toContainText('1.000,00')
|
||
})
|
||
|
||
test('Ausgabe buchen und Saldo fortschreiben', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
|
||
await page.selectOption('#art', 'ausgabe')
|
||
await page.fill('#datum', '2026-03-18')
|
||
await page.fill('#betrag', '49,90')
|
||
await page.fill('#empfaenger', 'Supermarkt')
|
||
await page.selectOption('#zuordnung', { label: KATEGORIE })
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
const zeile = page.locator('tr', { hasText: 'Supermarkt' })
|
||
await expect(zeile).toContainText('-49,90')
|
||
await expect(zeile).toContainText('950,10')
|
||
await expect(page.locator('.saldoanzeige').first()).toContainText('950,10')
|
||
})
|
||
|
||
test('Umbuchung erscheint auf beiden Kontoblättern', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
|
||
// Die Umbuchung entsteht im Kategoriefeld: Unter den Kategorien stehen
|
||
// die eigenen Konten in eckigen Klammern.
|
||
await page.selectOption('#art', 'ausgabe')
|
||
await page.fill('#datum', '2026-04-01')
|
||
await page.fill('#betrag', '500')
|
||
await page.fill('#empfaenger', `Übertrag ${MARKE}`)
|
||
await page.selectOption('#zuordnung', { label: `[${SPAR}]` })
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
// Ausgangskonto: Abgang, mit dem Gegenkonto in eckigen Klammern.
|
||
const hin = page.locator('tr', { hasText: `Übertrag ${MARKE}` })
|
||
await expect(hin).toContainText('-500,00')
|
||
await expect(hin).toContainText(`[${SPAR}]`)
|
||
|
||
// Gegenkonto: derselbe Vorgang mit umgekehrtem Vorzeichen – und mit dem
|
||
// Konto, von dem er kommt. Ohne diese Angabe stünde dort eine Umbuchung
|
||
// ohne erkennbares Gegenüber.
|
||
await page.goto(`/konten/${sparId}`)
|
||
const her = page.locator('tr', { hasText: `Übertrag ${MARKE}` })
|
||
await expect(her).toContainText('500,00')
|
||
await expect(her).toContainText('Umbuchung')
|
||
await expect(her).toContainText(`[${GIRO}]`)
|
||
await expect(page.locator('.saldoanzeige').first()).toContainText('5.500,00')
|
||
|
||
// Auch die Gegenseite ist anklickbar: Sie führt zu ihrer Buchung, die
|
||
// auf dem anderen Konto liegt und nur dort geändert wird.
|
||
await her.locator('a.zuordnungsfeld.gegenseite').click()
|
||
await expect(page).toHaveURL(/\/buchungen\/\d+/)
|
||
await expect(page.locator('h1')).toContainText('Buchung bearbeiten')
|
||
await expect(page.locator('#zuordnung')).toHaveValue(/^konto:\d+$/)
|
||
})
|
||
|
||
test('Kategorie im Kontoblatt zuordnen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
await page.fill('#datum', '2026-04-03')
|
||
await page.fill('#betrag', '12,30')
|
||
await page.fill('#empfaenger', `Ohne Kategorie ${MARKE}`)
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
const zeile = page.locator('tr', { hasText: `Ohne Kategorie ${MARKE}` })
|
||
await zeile.locator('a.zuordnungsfeld').click()
|
||
|
||
// Die Auswahlliste gibt es einmal und wandert in die angeklickte Zelle.
|
||
const liste = zeile.locator('#zuordnungsauswahl select')
|
||
await expect(liste).toBeVisible()
|
||
await liste.selectOption({ label: KATEGORIE })
|
||
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung')
|
||
await expect(page.locator('tr', { hasText: `Ohne Kategorie ${MARKE}` }))
|
||
.toContainText(KATEGORIE)
|
||
})
|
||
|
||
test('Klasse im Kontoblatt zuordnen und wieder löschen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
await page.fill('#datum', '2026-04-06')
|
||
await page.fill('#betrag', '8,80')
|
||
await page.fill('#empfaenger', `Ohne Klasse ${MARKE}`)
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
// Dieselbe Bauart wie bei der Zuordnung: die Liste wandert in die Zelle.
|
||
const zeile = page.locator('tr', { hasText: `Ohne Klasse ${MARKE}` })
|
||
await zeile.locator('a.klassenfeld').click()
|
||
|
||
const liste = zeile.locator('#klassenauswahl select')
|
||
await expect(liste).toBeVisible()
|
||
await liste.selectOption({ label: KLASSE })
|
||
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('Klasse gespeichert')
|
||
await expect(page.locator('tr', { hasText: `Ohne Klasse ${MARKE}` }))
|
||
.toContainText(KLASSE)
|
||
|
||
// Der erste Eintrag der Liste löscht die vorhandene Klasse wieder.
|
||
const danach = page.locator('tr', { hasText: `Ohne Klasse ${MARKE}` })
|
||
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('tr', { hasText: `Ohne Klasse ${MARKE}` }))
|
||
.not.toContainText(KLASSE)
|
||
})
|
||
|
||
test('Umbuchung im Kontoblatt zuordnen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
await page.fill('#datum', '2026-04-04')
|
||
await page.fill('#betrag', '75')
|
||
await page.fill('#empfaenger', `Nachträglich ${MARKE}`)
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
const zeile = page.locator('tr', { hasText: `Nachträglich ${MARKE}` })
|
||
await zeile.locator('a.zuordnungsfeld').click()
|
||
await zeile.locator('#zuordnungsauswahl select').selectOption({ label: `[${SPAR}]` })
|
||
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('Zuordnung')
|
||
await expect(page.locator('tr', { hasText: `Nachträglich ${MARKE}` }))
|
||
.toContainText(`[${SPAR}]`)
|
||
|
||
// Und die Gegenseite steht sofort auf dem anderen Konto.
|
||
await page.goto(`/konten/${sparId}`)
|
||
await expect(page.locator('tr', { hasText: `Nachträglich ${MARKE}` }))
|
||
.toContainText(`[${GIRO}]`)
|
||
})
|
||
|
||
test('Splitbuchung aus dem Kontoblatt heraus aufteilen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
await page.fill('#datum', '2026-04-05')
|
||
await page.fill('#betrag', '90')
|
||
await page.fill('#empfaenger', `Aufzuteilen ${MARKE}`)
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
const zeile = page.locator('tr', { hasText: `Aufzuteilen ${MARKE}` })
|
||
await zeile.locator('a.zuordnungsfeld').click()
|
||
await zeile.locator('#zuordnungsauswahl select').selectOption('split')
|
||
|
||
const dialog = page.locator('#splitdialog')
|
||
await expect(dialog).toBeVisible()
|
||
await expect(dialog).toContainText('-90,00')
|
||
|
||
await dialog.locator('select[name="split[0][zuordnung]"]').selectOption({ label: KATEGORIE })
|
||
await dialog.locator('input[name="split[0][betrag]"]').fill('-50')
|
||
await dialog.locator('select[name="split[1][zuordnung]"]').selectOption({ label: `[${SPAR}]` })
|
||
await dialog.locator('input[name="split[1][betrag]"]').fill('-40')
|
||
|
||
// Der Dialog rechnet den Restbetrag mit; er muss aufgehen.
|
||
await expect(page.locator('#splitdialogsumme')).not.toContainText('Rest')
|
||
|
||
// 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('tr', { hasText: `Aufzuteilen ${MARKE}` }).first())
|
||
.toContainText('Splitbuchung')
|
||
|
||
// Die Zeile mit Gegenkonto erscheint anteilig auf dem anderen Konto.
|
||
await page.goto(`/konten/${sparId}`)
|
||
await expect(page.locator('tr', { hasText: `Aufzuteilen ${MARKE}` })).toContainText('40,00')
|
||
})
|
||
|
||
test('Splitbuchung öffnet den Dialog und lässt sich dort löschen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
await page.fill('#datum', '2026-04-06')
|
||
await page.fill('#betrag', '70')
|
||
await page.fill('#empfaenger', `Wieder einfach ${MARKE}`)
|
||
await page.selectOption('select[name="split[0][zuordnung]"]', { label: KATEGORIE })
|
||
await page.fill('input[name="split[0][betrag]"]', '-30')
|
||
await page.selectOption('select[name="split[1][zuordnung]"]', { label: KATEGORIE })
|
||
await page.fill('input[name="split[1][betrag]"]', '-40')
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
// Der Klick führt nicht in die Auswahlliste, sondern in den Dialog –
|
||
// mit den vorhandenen Zeilen darin.
|
||
const zeile = page.locator('tr', { hasText: `Wieder einfach ${MARKE}` })
|
||
await zeile.locator('a.zuordnungsfeld').click()
|
||
|
||
const dialog = page.locator('#splitdialog')
|
||
await expect(dialog).toBeVisible()
|
||
await expect(page.locator('#zuordnungsauswahl select')).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')
|
||
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()
|
||
})
|
||
|
||
test('Splitbuchung mit zwei Zeilen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
|
||
await page.fill('#datum', '2026-05-02')
|
||
await page.fill('#betrag', '260,49')
|
||
await page.fill('#empfaenger', `Versicherung ${MARKE}`)
|
||
|
||
await page.selectOption('select[name="split[0][zuordnung]"]', { label: KATEGORIE })
|
||
await page.fill('input[name="split[0][notiz]"]', 'Kasko')
|
||
await page.fill('input[name="split[0][betrag]"]', '-162,51')
|
||
|
||
await page.selectOption('select[name="split[1][zuordnung]"]', { label: KATEGORIE })
|
||
await page.fill('input[name="split[1][notiz]"]', 'Haftpflicht')
|
||
await page.fill('input[name="split[1][betrag]"]', '-97,98')
|
||
|
||
// Die Maske rechnet den Restbetrag mit; er muss aufgehen.
|
||
await expect(page.locator('#splitsumme')).not.toContainText('Rest')
|
||
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
const zeile = page.locator('tr', { hasText: `Versicherung ${MARKE}` })
|
||
await expect(zeile).toContainText('Splitbuchung')
|
||
await expect(zeile).toContainText('-260,49')
|
||
|
||
// In der Spalte steht nur „Splitbuchung“ – wohin das Geld geht, zeigt
|
||
// der Tooltip: je Zeile Kategorie und Betrag.
|
||
const titel = await zeile.locator('a.zuordnungsfeld').getAttribute('title')
|
||
expect(titel).toContain(`${KATEGORIE}: -162,51`)
|
||
expect(titel).toContain(`${KATEGORIE}: -97,98`)
|
||
})
|
||
|
||
test('Buchungszeilen sind gleich hoch, abgeschnittene Texte stehen im Tooltip', async ({ page }) => {
|
||
const LANG = `Verwendungszweck ${MARKE} mit sehr vielen Wörtern, `.repeat(6).trim()
|
||
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
await page.fill('#datum', '2026-05-04')
|
||
await page.fill('#betrag', '11,11')
|
||
await page.fill('#empfaenger', `Langtext ${MARKE}`)
|
||
await page.fill('#buchungstext', LANG)
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
// Einheitlich zwei Textzeilen hoch: Eine Zeile, die mit ihrem Text
|
||
// wächst, ließe das Kontoblatt beim Blättern hüpfen.
|
||
const hoehen = await page.locator('table.kontoblatt tbody tr').evaluateAll(
|
||
(zeilen) => zeilen.map((zeile) => Math.round(zeile.getBoundingClientRect().height))
|
||
)
|
||
expect(hoehen.length).toBeGreaterThan(1)
|
||
// Ein Pixel Toleranz: Die letzte Zeile hat keine Trennlinie.
|
||
expect(Math.max(...hoehen) - Math.min(...hoehen)).toBeLessThanOrEqual(1)
|
||
|
||
const zeile = page.locator('tr', { hasText: `Langtext ${MARKE}` })
|
||
|
||
// Der abgeschnittene Buchungstext steht vollständig im Tooltip …
|
||
await expect(zeile.locator('td.buchungstext .zellentext'))
|
||
.toHaveAttribute('title', LANG)
|
||
|
||
// … der kurze Empfänger daneben bekommt keinen: Dort fehlt nichts.
|
||
await expect(zeile.locator('td').nth(3).locator('.zellentext'))
|
||
.not.toHaveAttribute('title')
|
||
})
|
||
|
||
test('Splitzeilen, die nicht aufgehen, werden abgewiesen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}/buchung`)
|
||
|
||
await page.fill('#datum', '2026-05-03')
|
||
await page.fill('#betrag', '100')
|
||
await page.fill('#empfaenger', `Krumm ${MARKE}`)
|
||
await page.selectOption('select[name="split[0][zuordnung]"]', { label: KATEGORIE })
|
||
await page.fill('input[name="split[0][betrag]"]', '-60')
|
||
|
||
await expect(page.locator('#splitsumme')).toContainText('Rest')
|
||
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
|
||
await expect(page.locator('.meldung-fehler')).toBeVisible()
|
||
await expect(page.locator('.meldung-fehler')).toContainText('Splitzeilen')
|
||
})
|
||
|
||
test('Kontoblatt lässt sich durchsuchen', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}`)
|
||
|
||
await page.fill('input[name="suche"]', 'Supermarkt')
|
||
await page.click('.filterleiste button[type="submit"]')
|
||
|
||
// Auf der Seite steht noch eine zweite Tabelle: die leeren Zeilen des
|
||
// Splitdialogs. Gemeint ist hier das Kontoblatt.
|
||
await expect(page.locator('table.kontoblatt tbody tr')).toHaveCount(1)
|
||
await expect(page.locator('table.kontoblatt tbody')).toContainText('Supermarkt')
|
||
// Mit Filter ist die Saldospalte leer – ein laufender Saldo wäre gelogen.
|
||
// Spalte 0 ist das Ankreuzfeld der Mehrfachauswahl.
|
||
await expect(page.locator('table.kontoblatt tbody tr td').nth(8)).toContainText('–')
|
||
})
|
||
|
||
test('Eine Buchung ohne Kategorie setzt ihr Konto in der Übersicht fett', async ({ page }) => {
|
||
// Heutiges Datum: Fett stehen nur Konten mit offenen Buchungen aus dem
|
||
// laufenden Jahr – ein festes Datum wäre nächstes Silvester falsch.
|
||
const heute = new Date().toISOString().slice(0, 10)
|
||
|
||
await page.goto(`/konten/${sparId}/buchung`)
|
||
await page.selectOption('#art', 'ausgabe')
|
||
await page.fill('#datum', heute)
|
||
await page.fill('#betrag', '17,50')
|
||
await page.fill('#empfaenger', 'Noch zuzuordnen')
|
||
// Kategorie bleibt „— keine —“: genau der Zustand, den ein automatisch
|
||
// gebuchter Bankumsatz ohne passende Regel hinterlässt.
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
|
||
await page.goto('/konten')
|
||
await expect(page.locator('tr.unkategorisiert', { hasText: SPAR })).toHaveCount(1)
|
||
await expect(page.locator('tr.unkategorisiert a', { hasText: SPAR }))
|
||
.toHaveAttribute('title', /ohne Kategorie/)
|
||
})
|
||
|
||
test('Löschen fragt nach – trotz strenger Content-Security-Policy', async ({ page }) => {
|
||
await page.goto(`/konten/${giroId}`)
|
||
await page.locator('tr', { hasText: 'Supermarkt' }).getByRole('link').first().click()
|
||
|
||
await expect(page).toHaveURL(/\/buchungen\/\d+/)
|
||
|
||
// Der Dialog MUSS kommen: die Bestätigung hängt an data-bestaetigen und
|
||
// wird von /static/app.js gesetzt, weil Inline-Handler an der CSP
|
||
// scheitern. Der Handler muss vor dem Klick stehen und den Dialog
|
||
// sofort schließen – ein offener Dialog blockiert die Klickaktion.
|
||
const gefragt = []
|
||
page.on('dialog', async (dialog) => {
|
||
gefragt.push(dialog.message())
|
||
await dialog.accept()
|
||
})
|
||
|
||
await page.locator('form[action$="/loeschen"] button').click()
|
||
|
||
expect(gefragt.join(' ')).toContain('löschen')
|
||
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht')
|
||
await expect(page.locator('table.kontoblatt tbody')).not.toContainText('Supermarkt')
|
||
})
|
||
|
||
test('Mehrere Buchungen über die Auswahl im Kontoblatt löschen', async ({ page }) => {
|
||
for (const empfaenger of ['Sammellöschung A', 'Sammellöschung B']) {
|
||
await page.goto(`/konten/${sparId}/buchung`)
|
||
await page.selectOption('#art', 'ausgabe')
|
||
await page.fill('#datum', '2026-04-02')
|
||
await page.fill('#betrag', '9,90')
|
||
await page.fill('#empfaenger', empfaenger)
|
||
await page.click('.buchungsmaske button[type="submit"]')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
}
|
||
|
||
await page.goto(`/konten/${sparId}`)
|
||
|
||
// Ohne Auswahl bleibt der Knopf gesperrt.
|
||
const knopf = page.locator('#buchungsauswahl button')
|
||
await expect(knopf).toBeDisabled()
|
||
|
||
// Das Ankreuzfeld in der Titelzeile wählt alle angezeigten Buchungen.
|
||
const anzahl = await page.locator('table.kontoblatt tbody input[name="buchungen[]"]').count()
|
||
await page.check('.alle-waehlen')
|
||
await expect(knopf).toContainText(`(${anzahl})`)
|
||
|
||
// Wie beim einzelnen Löschen hängt die Rückfrage an data-bestaetigen –
|
||
// der Handler muss vor dem Klick stehen.
|
||
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.getByText('Sammellöschung')).toHaveCount(0)
|
||
})
|
||
})
|