test: Ende-zu-Ende-Tests mit Playwright
13 Tests gegen die gebaute Anwendung mit eigener Datenbank auf Port 3111 - also durch Middleware, Authentifizierung, Validierung und Datenbank hindurch. Genau die Schicht, die Unit-Tests nicht erreichen. Laufzeit rund 20 Sekunden. api.spec.ts prueft, was bisher nur von Hand geprueft war: - Einrichtung sperrt sich nach dem ersten Konto - ohne Anmeldung liefert jeder Endpunkt 401 - fremdes Origin und falscher Content-Type werden abgewiesen - SSRF-Sperren greifen - Datentrennung: ein zweites Konto bekommt auf fremde Projekte, Aufgaben und Adminwege durchgaengig 404 beziehungsweise 403, das Original bleibt unveraendert - Aufgaben: Reihenfolge, abgeleiteter Fortschritt, untergeschobene IDs - Export ohne Geheimnisse, Import je Strategie ui.spec.ts geht den Weg eines Menschen: anmelden, Projekt anlegen, Aufgaben anlegen und ziehen, suchen, abmelden. Das Ziehen wird nach einem Neuladen erneut geprueft - sonst waere nur die Anzeige getestet, nicht das Speichern. Zwei Vorkehrungen: - global-setup.ts bricht ab, wenn der Datenbankname nicht "test" enthaelt, und zwar vor dem TRUNCATE. Der teuerste denkbare Fehler waere eine Testreihe, die die Produktivdaten leert. - Playwright startet den Server selbst mit eigener DATABASE_URL und eigenem Port; eine laufende Instanz auf 3000 stoert nicht. Beim Schreiben zeigte sich, dass synthetische Zieh-Gesten eine Pause vor dem Loslassen brauchen - dnd-kit wertet die Bewegung in einem eigenen Frame aus und faende sonst kein Ziel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Ein Weg durch die Oberflaeche, wie ihn ein Mensch geht: anmelden, Projekt
|
||||
* anlegen, Aufgaben verwalten. Keine Vollabdeckung – die Wege, die kaputt
|
||||
* gehen duerfen, ohne dass es jemand merkt.
|
||||
*
|
||||
* Setzt voraus, dass api.spec.ts das Konto bereits angelegt hat; laeuft
|
||||
* alphabetisch danach.
|
||||
*/
|
||||
|
||||
const KONTO = { username: "chefin", password: "ein-langes-testpasswort" };
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await page.fill("#login-username", KONTO.username);
|
||||
await page.fill("#login-password", KONTO.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.getByText("Vibe Coding Kontrollzentrum")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Dashboard zeigt Kennzahlen und Projekte", async ({ page }) => {
|
||||
await expect(page.getByRole("heading", { name: /Deine Projekte/ })).toBeVisible();
|
||||
await expect(page.getByText("Projekte", { exact: false }).first()).toBeVisible();
|
||||
// Keine unbeabsichtigten Fehler in der Konsole.
|
||||
expect(page.url()).toContain("/");
|
||||
});
|
||||
|
||||
test("Projekt anlegen und wiederfinden", async ({ page }) => {
|
||||
await page.getByRole("button", { name: /Neues Projekt/ }).click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
|
||||
await page.fill("#project-name", "Oberflaechentest");
|
||||
await page.fill("#project-summary", "Von Playwright angelegt.");
|
||||
await page.getByRole("button", { name: "Projekt anlegen" }).click();
|
||||
|
||||
await expect(page.getByRole("dialog")).toBeHidden();
|
||||
await expect(page.getByText("Oberflaechentest")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Aufgaben anlegen und per Ziehen verschieben", async ({ page }) => {
|
||||
await page.getByRole("link", { name: /Projekt Oberflaechentest/ }).click();
|
||||
await expect(page.getByRole("heading", { name: "Oberflaechentest" })).toBeVisible();
|
||||
|
||||
// Zwei Aufgaben ueber den Dialog anlegen.
|
||||
for (const titel of ["Erste Aufgabe", "Zweite Aufgabe"]) {
|
||||
await page.getByRole("button", { name: "Aufgabe", exact: true }).click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await page.fill("#task-title", titel);
|
||||
await page.getByRole("button", { name: "Anlegen" }).click();
|
||||
await expect(page.getByRole("dialog")).toBeHidden();
|
||||
}
|
||||
|
||||
const offen = page.locator('[data-task-column="TODO"]');
|
||||
const inArbeit = page.locator('[data-task-column="DOING"]');
|
||||
await expect(offen.getByText("Erste Aufgabe")).toBeVisible();
|
||||
await expect(offen.getByText("Zweite Aufgabe")).toBeVisible();
|
||||
|
||||
// Erste Aufgabe nach "In Arbeit" ziehen.
|
||||
const griff = offen.locator('button[aria-label$="verschieben"]').first();
|
||||
const von = await griff.boundingBox();
|
||||
const nach = await inArbeit.boundingBox();
|
||||
expect(von && nach).toBeTruthy();
|
||||
|
||||
await page.mouse.move(von!.x + von!.width / 2, von!.y + von!.height / 2);
|
||||
await page.mouse.down();
|
||||
// Erst ueber die Aktivierungsschwelle des Sensors (5 Pixel) hinaus.
|
||||
await page.mouse.move(von!.x + 30, von!.y + 10, { steps: 6 });
|
||||
await page.mouse.move(nach!.x + nach!.width / 2, nach!.y + 60, { steps: 20 });
|
||||
// dnd-kit wertet die Bewegung in einem eigenen Frame aus. Ohne diese
|
||||
// Pause faende das Loslassen noch kein Ziel und die Karte spraenge zurueck.
|
||||
await page.waitForTimeout(250);
|
||||
await page.mouse.up();
|
||||
|
||||
await expect(inArbeit.getByText("Erste Aufgabe")).toBeVisible();
|
||||
|
||||
// Und der Server hat es auch so gespeichert.
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.locator('[data-task-column="DOING"]').getByText("Erste Aufgabe"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Volltextsuche findet Notizen", async ({ page }) => {
|
||||
await page.getByRole("link", { name: /Projekt Oberflaechentest/ }).click();
|
||||
await page.getByRole("button", { name: "Notiz", exact: true }).click();
|
||||
|
||||
await page.getByPlaceholder(/Gedanken/).fill("Suchbegriff Zaunkoenig steht hier.");
|
||||
await page.getByRole("button", { name: "Speichern" }).click();
|
||||
await expect(page.getByText("Zaunkoenig", { exact: false })).toBeVisible();
|
||||
|
||||
// exact, sonst trifft es auch "Zurueck zum Dashboard".
|
||||
await page.getByRole("link", { name: "Dashboard", exact: true }).click();
|
||||
await page.getByPlaceholder(/durchsuchen/).fill("Zaunkoenig");
|
||||
|
||||
await expect(page.getByText("Treffer in Notizen")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Abmelden fuehrt zurueck zur Anmeldung", async ({ page }) => {
|
||||
await page.getByRole("button", { name: /Benutzermenue/ }).click();
|
||||
await page.getByRole("menuitem", { name: "Abmelden" }).click();
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// Und die geschuetzte Seite ist wieder zu.
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
Reference in New Issue
Block a user