Budgetplaner mit Monatswerten je Kategorie, Monatsabschluss mit Prüflauf, Snapshot und Buchungssperre. Dazu drei Vorarbeiten in der Oberfläche. Budgetplaner - Migration 004: budgets, budget_positionen, monatsabschluesse - Zeilen sind der Kategoriebaum, Spalten die Monate; die ganze Tabelle wird in einem Formular gespeichert, damit mehrere Änderungen zusammen ankommen - Zwei Ansichten: Verlauf (Jahressumme, abgelaufene Monate zusammengefasst, laufender Monat mit Budget/Ist/Saldo, danach Folgemonate) und alle Monate - Ausgaben ohne Vorzeichen eingeben, es kommt aus der Kategorieart. Saldo ist Ist minus Budget: positiv heißt besser als geplant, negativ ist die Überschreitung und wird rot ausgewiesen - Übergeordnete Kategorien summieren ihre Nachfahren und sind nicht beplanbar - Ein Monat ohne Position ist "nicht geplant" und damit etwas anderes als "mit 0,00 geplant" - Wert auf Folgemonate übertragen, Budget als Kopie ins neue Jahr Monatsabschluss - Prüflauf über nicht kategorisierte Buchungen, nicht abgeglichene Umsätze und Splitsummen-Differenzen; er hält niemanden auf, sondern zeigt den Stand - Snapshot der Kontosalden und Kategoriesummen als JSON, bleibt auch nach einer Wiedereröffnung stehen - Abgeschlossene Monate sperren Buchungen in beide Richtungen: weder hinein noch heraus verschieben. Abschaltbar über monatsabschluss_sperrt_buchungen Vorarbeiten - .knopf.primaer trug einen eigenen margin-top und stand dadurch in jeder Knopfleiste tiefer als seine Nachbarn - sichtbar an "Abbrechen" neben "Konto anlegen". Der Abstand liegt jetzt allein bei der Knopfleiste - Kontengruppen werden wie Konten und Kategorien auf einer eigenen Formularseite gepflegt statt inline; dort hängt auch die Zuordnung der Konten (ankreuzen statt jedes Konto einzeln zu öffnen) - Der QIF-Export enthält keine Kontengruppen: kein Feld im !Account-Eintrag, nichts in der QSD-Datei. Import-Oberfläche und Doku sagen das jetzt - Die Baum-Einrückung lief über ein style-Attribut und wurde von der Content-Security-Policy verworfen; sie steht jetzt als data-ebene im Markup. Aus demselben Grund blendete app.js Erfolgsmeldungen nie aus Tests: 11_BudgetTest, 12_MonatsabschlussTest (76 Prüfungen), E2E planung.spec.js. Einzelheiten in docs/planung.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
134 lines
5.5 KiB
JavaScript
134 lines
5.5 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'
|
||
|
||
// Eindeutige Namen je Lauf: die Tests arbeiten auf einer bestehenden Instanz
|
||
// und dürfen sich nicht an Resten eines früheren Laufs stoßen.
|
||
const MARKE = Date.now().toString().slice(-6)
|
||
|
||
// Ein Jahr weit in der Zukunft: dort sind alle zwölf Monate Folgemonate, die
|
||
// Spaltenaufteilung des Planers bleibt damit über die Jahre stabil.
|
||
const JAHR = new Date().getFullYear() + 5
|
||
|
||
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(/\/$/)
|
||
}
|
||
|
||
/**
|
||
* Legt ein Budget an und liefert dessen Id. Nach dem Anlegen steht sie in der
|
||
* URL des Planers – die Auswahlliste enthält alle Budgets der Instanz und
|
||
* taugt deshalb nicht zum Wiederfinden.
|
||
*/
|
||
async function budgetAnlegen(page, name) {
|
||
await page.goto('/planung/budget/verwaltung')
|
||
await page.fill('#name', name)
|
||
await page.fill('#jahr', String(JAHR))
|
||
await page.click('.formular button:has-text("Budget anlegen")')
|
||
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||
await expect(page).toHaveURL(/budget=\d+/)
|
||
|
||
return new URL(page.url()).searchParams.get('budget')
|
||
}
|
||
|
||
/** Die erste Kategoriezeile mit Eingabefeldern – eine Blattkategorie. */
|
||
function ersteBeplanbareZeile(page) {
|
||
return page.locator('tr.kategoriezeile:not(.summenzeile-kategorie)').first()
|
||
}
|
||
|
||
test.describe('Planung', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await anmelden(page)
|
||
})
|
||
|
||
test('Planungsübersicht führt zu Budget und Monatsabschluss', async ({ page }) => {
|
||
await page.click('.seitenleiste a[href="/planung"]')
|
||
|
||
await expect(page.locator('h1')).toContainText('Planung')
|
||
await expect(page.locator('a[href="/planung/budget"]').first()).toBeVisible()
|
||
await expect(page.locator('a[href="/planung/monatsabschluss"]').first()).toBeVisible()
|
||
})
|
||
|
||
test('Budget anlegen, Monatswert eintragen und wiederfinden', async ({ page }) => {
|
||
const id = await budgetAnlegen(page, `E2E-Budget ${MARKE}`)
|
||
|
||
await page.goto(`/planung/budget?budget=${id}&ansicht=alle`)
|
||
|
||
const zeile = ersteBeplanbareZeile(page)
|
||
const kategorie = (await zeile.locator('.baumname').innerText()).trim()
|
||
|
||
await zeile.locator('input.betragsfeld').first().fill('250,00')
|
||
await page.click('button:has-text("Budget speichern")')
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('Budget gespeichert')
|
||
|
||
// Nach dem Speichern steht der Wert wieder im Feld – und in der Jahressumme.
|
||
const wieder = page.locator('tr.kategoriezeile', { hasText: kategorie }).first()
|
||
await expect(wieder.locator('input.betragsfeld').first()).toHaveValue('250,00')
|
||
await expect(wieder).toContainText('250,00')
|
||
})
|
||
|
||
test('Wert auf die Folgemonate übertragen', async ({ page }) => {
|
||
const id = await budgetAnlegen(page, `E2E-Folgemonate ${MARKE}`)
|
||
|
||
await page.goto(`/planung/budget?budget=${id}&ansicht=alle`)
|
||
|
||
const zeile = ersteBeplanbareZeile(page)
|
||
const kategorie = (await zeile.locator('.baumname').innerText()).trim()
|
||
|
||
await zeile.locator('input.betragsfeld').first().fill('100,00')
|
||
await zeile.locator('button[name="uebertragen"]').click()
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('Folgemonate')
|
||
|
||
const wieder = page.locator('tr.kategoriezeile', { hasText: kategorie }).first()
|
||
const felder = wieder.locator('input.betragsfeld')
|
||
await expect(felder.nth(0)).toHaveValue('100,00')
|
||
await expect(felder.nth(11)).toHaveValue('100,00')
|
||
// Zwölf Monate à 100,00 stehen als Jahressumme in der Zeile.
|
||
await expect(wieder).toContainText('1.200,00')
|
||
})
|
||
|
||
test('Monat abschließen, Snapshot ansehen und wiedereröffnen', async ({ page }) => {
|
||
// Ein Monat weit in der Vergangenheit, den kein anderer Test anfasst.
|
||
const jahr = new Date().getFullYear() - 9
|
||
|
||
// Rückfragen bejahen: Wiedereröffnen und Abschließen fragen beide nach.
|
||
page.on('dialog', d => d.accept())
|
||
|
||
await page.goto(`/planung/monatsabschluss?jahr=${jahr}&monat=6`)
|
||
await expect(page.locator('h2').first()).toContainText('Prüflauf')
|
||
|
||
// Ein früherer Lauf kann den Monat abgeschlossen zurückgelassen haben.
|
||
if (await page.locator('button:has-text("Monat wiedereröffnen")').count()) {
|
||
await page.click('button:has-text("Monat wiedereröffnen")')
|
||
await expect(page.locator('button:has-text("Monat abschließen")')).toBeVisible()
|
||
}
|
||
|
||
await page.fill('#notiz', `E2E ${MARKE}`)
|
||
await page.click('button:has-text("Monat abschließen")')
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('abgeschlossen')
|
||
|
||
// Der abgeschlossene Monat weist die Sperre aus und bietet den Snapshot an.
|
||
await expect(page.locator('.phasenmarke')).toContainText('abgeschlossen')
|
||
await page.click('a:has-text("Snapshot ansehen")')
|
||
await expect(page.locator('h1')).toContainText('Abschluss')
|
||
await expect(page.locator('.summenzeile')).toContainText('Summe Kontostände')
|
||
|
||
await page.goto(`/planung/monatsabschluss?jahr=${jahr}&monat=6`)
|
||
await page.click('button:has-text("Monat wiedereröffnen")')
|
||
await expect(page.locator('.meldung-erfolg')).toContainText('wieder offen')
|
||
await expect(page.locator('button:has-text("Monat abschließen")')).toBeVisible()
|
||
})
|
||
})
|