Files

259 lines
10 KiB
TypeScript
Raw Permalink 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.
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();
});
});