import { expect, test } from "@playwright/test"; import { anmelden, eigenerName } from "./wehr"; /** * E2E-Tests für Phase 9 — die Kassenverwaltung. * * Was hier geprüft wird, ist nicht die Kür, sondern das, wovon die * Kassenprüfung abhängt: * * * die Übersicht zeigt Bestände, die aus den Buchungen gerechnet sind * * eine Buchung entsteht und erscheint im Kassenbuch * * eine festgeschriebene Buchung lässt sich nicht mehr ändern — die * Oberfläche bietet Gegenbuchen an, nicht Bearbeiten * * die Beitragsliste zeigt Rückstände, nicht nur Summen * * eine Rechnung entsteht als Entwurf mit fortlaufender Nummer * * der geprüfte Zeitraum des Vorjahres ist zu * * Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet * (`npm run db:test-reset`). */ test.describe.configure({ mode: "serial" }); const JAHR = new Date().getFullYear(); test.describe("Kassenübersicht", () => { test("zeigt die Konten mit gerechnetem Bestand", async ({ page }) => { await anmelden(page); await page.goto("/kasse"); await expect(page.getByRole("heading", { name: "Kasse", level: 1 })).toBeVisible(); // Aus dem Seed — die Überschriften der Kontenkarten, nicht irgendein // Vorkommen des Wortes: "Barkasse" steht auch in Verwendungszwecken. await expect(page.getByRole("heading", { name: "Barkasse" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Girokonto Raiffeisenbank" })).toBeVisible(); await expect( page.getByRole("heading", { name: "Rücklage Fahrzeugbeschaffung" }), ).toBeVisible(); // Ein Bestand steht da, und zwar als Betrag — nicht als leeres Feld. await expect(page.getByText(/\d+,\d{2}\s€/).first()).toBeVisible(); }); test("führt zu den vier Aufgaben der Kasse", async ({ page }) => { await anmelden(page); await page.goto("/kasse"); for (const [name, pfad] of [ ["Buchungen", "/kasse/buchungen"], ["Beiträge", "/kasse/beitraege"], ["Rechnungen", "/kasse/rechnungen"], ["Abschluss", "/kasse/abschluss"], ] as const) { await expect(page.locator(`a[href="${pfad}"]`).first()).toBeVisible(); expect(name.length).toBeGreaterThan(0); } }); }); test.describe("Kassenbuch", () => { test("zeigt die Buchungen mit Summen über alle Treffer", async ({ page }) => { await anmelden(page); await page.goto("/kasse/buchungen"); await expect(page.getByRole("heading", { name: "Buchungen", level: 1 })).toBeVisible(); // Die drei Summen oben. Sie beziehen sich auf alle Treffer, nicht nur auf // die angezeigte Seite — sonst bedeutete die Zahl nichts. await expect(page.getByText("Einnahmen", { exact: true })).toBeVisible(); await expect(page.getByText("Ausgaben", { exact: true })).toBeVisible(); await expect(page.getByText("Saldo", { exact: true })).toBeVisible(); // Aus dem Seed. await expect(page.getByRole("cell", { name: /Maifest/ }).first()).toBeVisible(); }); test("erfasst eine Buchung und zeigt sie danach im Kassenbuch", async ({ page }) => { const zweck = eigenerName("Buchung"); await anmelden(page); await page.goto("/kasse/buchungen"); await page.getByRole("button", { name: "Buchung erfassen" }).click(); await page.getByLabel("Betrag", { exact: true }).fill("42.50"); await page.getByLabel("Verwendungszweck").fill(zweck); // Kategorie wählen — die erste Ausgabenkategorie reicht. await page.getByLabel("Kategorie", { exact: true }).click(); await page.getByRole("option").first().click(); await page.getByRole("button", { name: "Buchen" }).click(); // In der Zeile mit dem eigenen Verwendungszweck — der Betrag allein wäre // nicht eindeutig, sobald ein zweiter Lauf dieselbe Buchung erzeugt hat. const zeile = page.getByRole("row", { name: new RegExp(zweck) }); await expect(zeile).toBeVisible(); await expect(zeile.getByRole("cell", { name: "−42,50 €" })).toBeVisible(); }); test("bietet für festgeschriebene Buchungen Gegenbuchen statt Bearbeiten an", async ({ page, }) => { await anmelden(page); await page.goto("/kasse/buchungen"); // Die Vorjahresbuchungen sind festgeschrieben — daran hängt die ganze // Kassenprüfung. Die Oberfläche muss das zeigen, nicht nur der Server // beim Versuch. Die Pressluftatmer gibt es im Seed nur im Vorjahr. const fest = page.getByRole("row", { name: /Pressluftatmer/ }); await expect(fest).toBeVisible(); await expect(fest.getByRole("button", { name: "Gegenbuchen" })).toBeVisible(); await expect(fest.getByRole("button", { name: "Verwerfen" })).toHaveCount(0); // Und die Gegenprobe: eine Buchung des laufenden Jahres ist noch ein // Entwurf und darf verworfen werden. const offen = page.getByRole("row", { name: /Kupplung MTW/ }); await expect(offen.getByRole("button", { name: "Verwerfen" })).toBeVisible(); await expect(offen.getByRole("button", { name: "Gegenbuchen" })).toHaveCount(0); }); test("erklärt beim Gegenbuchen, warum beide Buchungen stehen bleiben", async ({ page }) => { await anmelden(page); await page.goto("/kasse/buchungen"); await page .getByRole("row", { name: /Pressluftatmer/ }) .getByRole("button", { name: "Gegenbuchen" }) .click(); await expect(page.getByRole("dialog")).toContainText("Kassenprüfer"); // Ohne Grund geht es nicht weiter — er steht später im Verwendungszweck. await expect(page.getByRole("button", { name: "Gegenbuchen" }).last()).toBeDisabled(); }); }); test.describe("Beiträge", () => { test("zeigt Sollstellung, Offenes und die Rückstände", async ({ page }) => { await anmelden(page); await page.goto("/kasse/beitraege"); await expect(page.getByRole("heading", { name: "Mitgliedsbeiträge", level: 1 })).toBeVisible(); await expect(page.getByText("Sollstellung")).toBeVisible(); await expect(page.getByText("Bezahlt", { exact: true }).first()).toBeVisible(); // Die Sätze aus dem Seed. await expect(page.getByText("Aktive und Passive")).toBeVisible(); await expect(page.getByText("Jugendfeuerwehr").first()).toBeVisible(); }); test("zeigt die Vorschau des Beitragslaufs, bevor etwas entsteht", async ({ page }) => { await anmelden(page); await page.goto("/kasse/beitraege"); await page.getByRole("button", { name: `Beitragslauf ${JAHR}` }).click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Das Jahr ist im Seed bereits gelaufen — der Lauf darf deshalb nichts // doppelt anlegen und muss genau das sagen. await expect(dialog).toContainText(/bereits einen|0 Beiträge/); }); }); test.describe("Rechnungen", () => { test("zeigt die Rechnungen aus dem Bestand mit ihrem Stand", async ({ page }) => { await anmelden(page); await page.goto("/kasse/rechnungen"); await expect(page.getByRole("heading", { name: "Rechnungen", level: 1 })).toBeVisible(); await expect(page.getByRole("cell", { name: `${JAHR}-0001` })).toBeVisible(); await expect(page.getByRole("cell", { name: /Sonnleitner/ })).toBeVisible(); await expect(page.getByRole("cell", { name: /Trachtenverein/ })).toBeVisible(); }); test("schreibt eine Rechnung mit fortlaufender Nummer", async ({ page }) => { const empfaenger = eigenerName("Rechnung"); await anmelden(page); await page.goto("/kasse/rechnungen"); await page.getByRole("button", { name: "Rechnung schreiben" }).click(); await page.getByLabel("Empfänger").fill(empfaenger); await page.getByLabel("Bezeichnung").fill("Einsatz LF 20, angefangene Stunde"); await page.getByLabel("Einzelpreis").fill("396.00"); // Der Betrag wird gerechnet, nicht eingegeben — hier zur Anzeige, auf dem // Server noch einmal verbindlich. await expect(page.getByRole("dialog")).toContainText("396,00 €"); await page.getByRole("button", { name: "Als Entwurf anlegen" }).click(); await expect(page.getByText(/Rechnung \d{4}-\d{4} als Entwurf angelegt/)).toBeVisible(); await expect(page.getByRole("cell", { name: new RegExp(empfaenger) })).toBeVisible(); }); }); test.describe("Abschluss und Auswertung", () => { test("zeigt die Kassenprüfung des Vorjahres als festgeschrieben", async ({ page }) => { await anmelden(page); await page.goto("/kasse/abschluss"); await expect( page.getByRole("heading", { name: "Abschluss und Auswertung", level: 1 }), ).toBeVisible(); await expect(page.getByText("Festgeschrieben").first()).toBeVisible(); // Die Differenz aus dem Seed steht mit ihrer Erklärung da. Genau das ist // der Zweck des Feldes: in fünf Jahren weiß sonst niemand mehr, woher sie // kam. await expect(page.getByText("−3,20 €")).toBeVisible(); await expect(page.getByText(/Wechselgeld/)).toBeVisible(); }); test("weist einen Abschluss ab, dessen Zeitraum schon geprüft ist", async ({ page }) => { await anmelden(page); await page.goto("/kasse/abschluss"); await page.getByRole("button", { name: "Kassenprüfung anlegen" }).click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Der Dialog schlägt von sich aus das Vorjahr vor — und genau das ist // bereits geprüft. Erst die Kontenliste abwarten: sie kommt vom Server, // und ohne sie wären null Felder zu füllen. await expect(dialog.getByRole("columnheader", { name: "Laut Buch" })).toBeVisible(); const felder = dialog.getByRole("spinbutton"); const anzahl = await felder.count(); for (let stelle = 0; stelle < anzahl; stelle += 1) { await felder.nth(stelle).fill("100"); } await dialog.getByRole("button", { name: "Als Entwurf anlegen" }).click(); await expect(page.getByText(/überschneidet sich mit einem bereits geprüften/)).toBeVisible(); }); test("zeigt die Jahresauswertung mit Vorjahresvergleich", async ({ page }) => { await anmelden(page); await page.goto("/kasse/abschluss"); await page.getByRole("button", { name: "Auswertung" }).click(); await expect(page.getByText(`Verlauf ${JAHR}`)).toBeVisible(); await expect(page.getByText("Einnahmen nach Kategorie")).toBeVisible(); await expect(page.getByText("Ausgaben nach Kategorie")).toBeVisible(); // Eine Zahl ohne Vergleich sagt nichts — das Vorjahr steht daneben. await expect(page.getByText(new RegExp(`${JAHR - 1}:`)).first()).toBeVisible(); }); });