Files
nebula/e2e/ui.spec.ts
T
florian.wildeandClaude Opus 5 fce0a90cc3
CI / pruefen (push) Has been cancelled
chore: prisma.config.ts, Tests und Doku, Version 1.1.0
Prisma liest seine Einstellungen aus prisma.config.ts statt aus der
package.json; die Warnung vor Prisma 7 ist damit weg. Der dotenv-Aufruf
darin ist nicht optional: sobald die Datei existiert, laedt Prisma die .env
nicht mehr von selbst, und migrate deploy staende ohne DATABASE_URL da.

Vier neue Ende-zu-Ende-Tests: Reihenfolge und Mehrfachauswahl ueber die API,
Mehrfachauswahl in der Oberflaeche, der Aenderungsverlauf und das Manifest
samt Symbolen ohne Anmeldung. Damit sind es 21.

Der Test zum Ziehen von Aufgaben war gelegentlich rot, ohne dass etwas kaputt
war: das Neuladen brach den noch laufenden Speicheraufruf ab. Er wartet jetzt
auf die Antwort, statt auf eine feste Pause zu hoffen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:30:44 +02:00

206 lines
8.7 KiB
TypeScript
Raw 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";
/**
* 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);
// Auf die Antwort warten, bevor neu geladen wird. Sonst bricht das Neuladen
// den noch laufenden Aufruf ab, und der Test schlaegt gelegentlich fehl,
// obwohl die Anwendung in Ordnung ist – genau so ist es passiert.
const gespeichert = page.waitForResponse(
(response) => response.url().includes("/tasks/reorder") && response.request().method() === "PATCH",
);
await page.mouse.up();
await expect(inArbeit.getByText("Erste Aufgabe")).toBeVisible();
expect((await gespeichert).ok()).toBeTruthy();
// 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("Schnellsuche findet Projekt und Notiz", async ({ page }) => {
await page.keyboard.press("Control+k");
await expect(page.getByRole("dialog", { name: "Schnellsuche" })).toBeVisible();
// Ohne Eingabe stehen Projekte und Bereiche bereit.
await expect(page.getByRole("dialog").getByText("Oberflaechentest")).toBeVisible();
await page.getByPlaceholder(/Projekt, Notiz oder Bereich/).fill("Zaunkoenig");
await expect(page.getByRole("dialog").getByText(/in Oberflaechentest/)).toBeVisible();
// Enter springt zum ersten Treffer.
await page.keyboard.press("Enter");
await expect(page.getByRole("heading", { name: "Oberflaechentest" })).toBeVisible();
// Escape schliesst wieder.
await page.keyboard.press("Control+k");
await expect(page.getByRole("dialog", { name: "Schnellsuche" })).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog", { name: "Schnellsuche" })).toBeHidden();
});
test("Mehrfachauswahl aendert mehrere Projekte auf einmal", async ({ page }) => {
const kaestchen = page.locator('input[aria-label$="auswaehlen"]');
const anzahl = await kaestchen.count();
expect(anzahl).toBeGreaterThan(0);
// Ohne Auswahl gibt es keine Leiste – sie soll nicht dauerhaft im Weg sein.
await expect(page.getByText(/ausgewählt/)).toBeHidden();
await kaestchen.first().check();
await expect(page.getByText("1 ausgewählt")).toBeVisible();
await page.getByRole("button", { name: "Tags", exact: true }).click();
await page.fill("#bulk-tags", "sammeltest");
await page.getByRole("button", { name: "Hinzufügen" }).click();
await expect(page.getByText("#sammeltest").first()).toBeVisible();
// Nach getaner Arbeit ist die Auswahl aufgehoben.
await expect(page.getByText(/ausgewählt/)).toBeHidden();
});
test("Die Versionsnummer oeffnet den Aenderungsverlauf", async ({ page }) => {
await page.getByRole("button", { name: /^v\d+\.\d+\.\d+$/ }).click();
// Ueber die Rolle, nicht ueber den Text: das Wort steht auch in einem der
// Eintraege, und ein blosser Textvergleich waere mehrdeutig.
const dialog = page.getByRole("dialog", { name: "Änderungsverlauf" });
await expect(dialog).toBeVisible();
// Die laufende Fassung ist als solche gekennzeichnet.
await expect(dialog.getByText("installiert")).toBeVisible();
// Und es steht wirklich etwas drin.
await expect(dialog.getByRole("heading", { level: 3 }).first()).toBeVisible();
await expect(dialog.getByText("Neu").first()).toBeVisible();
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
});
test("Das Manifest liegt bereit, auch ohne Anmeldung", async ({ page, playwright }) => {
const anonym = await playwright.request.newContext({
baseURL: test.info().project.use.baseURL,
});
// Ohne diesen Weg an der Zugangsschranke vorbei liesse sich die Anwendung
// nicht installieren: der Browser holt das Manifest, bevor jemand angemeldet ist.
const antwort = await anonym.get("/manifest.webmanifest");
expect(antwort.ok()).toBeTruthy();
const manifest = await antwort.json();
expect(manifest.name).toContain("Nebula");
expect(manifest.display).toBe("standalone");
expect(manifest.icons.some((icon: { purpose: string }) => icon.purpose === "maskable")).toBe(true);
for (const icon of manifest.icons) {
const bild = await anonym.get(icon.src);
expect(bild.ok(), icon.src).toBeTruthy();
expect(bild.headers()["content-type"]).toContain("image/png");
}
await anonym.dispose();
// Die Seite muss ein Symbol verweisen. Fehlt es, sucht der Browser
// /favicon.ico – das es hier nicht gibt, und die Konsole fuellt sich mit
// 404ern. Genau das ist beim Ergaenzen des Apple-Symbols passiert.
await page.goto("/");
await expect(page.locator('link[rel="icon"]').first()).toHaveAttribute("href", /icon/);
});
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/);
});