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>
This commit is contained in:
+26
-2
@@ -138,6 +138,15 @@ Bedienung über Tastatur: Pfeile wählen, `Enter` öffnet, `Esc` schließt. Die
|
|||||||
wird beim ersten Öffnen geholt und dann behalten — sie bei jedem Tastendruck neu zu laden
|
wird beim ersten Öffnen geholt und dann behalten — sie bei jedem Tastendruck neu zu laden
|
||||||
wäre Verschwendung.
|
wäre Verschwendung.
|
||||||
|
|
||||||
|
### Mehrfachauswahl
|
||||||
|
|
||||||
|
Ein Kästchen auf jeder Karte — es erscheint erst beim Überfahren, damit nicht auf jeder
|
||||||
|
Karte dauerhaft eines steht. Sobald etwas ausgewählt ist, fährt unten eine Leiste ein:
|
||||||
|
**Status setzen**, **Tags ergänzen oder entfernen**, **als Favorit markieren**, **löschen**.
|
||||||
|
|
||||||
|
Was der Filter ausblendet, fällt aus der Auswahl. Sonst träfe eine Massenänderung Projekte,
|
||||||
|
die man gar nicht mehr vor Augen hat. Fremde IDs fallen serverseitig still unter den Tisch.
|
||||||
|
|
||||||
### Kanban
|
### Kanban
|
||||||
|
|
||||||
Spalten je Status, Karten per Ziehen verschiebbar. Der Wechsel wird sofort angezeigt und
|
Spalten je Status, Karten per Ziehen verschiebbar. Der Wechsel wird sofort angezeigt und
|
||||||
@@ -147,8 +156,15 @@ erst danach gespeichert; scheitert das Speichern, springt die Karte zurück.
|
|||||||
normaler Link.
|
normaler Link.
|
||||||
- Das Ziehen startet erst nach ein paar Pixeln Bewegung, damit Klicks weiter funktionieren.
|
- Das Ziehen startet erst nach ein paar Pixeln Bewegung, damit Klicks weiter funktionieren.
|
||||||
Auf Touchgeräten nach kurzem Halten.
|
Auf Touchgeräten nach kurzem Halten.
|
||||||
- Die Reihenfolge innerhalb einer Spalte folgt der gewählten Sortierung. Frei sortieren
|
- **Freie Reihenfolge innerhalb einer Spalte.** Die Position wird am Projekt gespeichert,
|
||||||
ginge nur mit einer gespeicherten Position — bewusst nicht gebaut.
|
wie beim Aufgabenbrett. Deshalb ist die Sortierung im Kopf in dieser Ansicht abgeschaltet
|
||||||
|
— sie stehen zu lassen, ohne dass sie wirkt, wäre irreführend.
|
||||||
|
- Reines Umsortieren fasst das Änderungsdatum **nicht** an. Sonst schöbe ein Aufräumen im
|
||||||
|
Brett sämtliche Projekte in der Sortierung „Zuletzt geändert“ nach oben.
|
||||||
|
- Neue, importierte und per Status verschobene Projekte landen am Ende ihrer Spalte.
|
||||||
|
- **Ansicht, Sortierung und Archivschalter überdauern das Neuladen** (im Browser
|
||||||
|
gespeichert, nicht am Konto). Der Suchbegriff bleibt bewusst außen vor — eine Suche von
|
||||||
|
gestern beim Öffnen wiederherzustellen wäre eher verwirrend als hilfreich.
|
||||||
|
|
||||||
### Projektkarte auf einen Blick
|
### Projektkarte auf einen Blick
|
||||||
|
|
||||||
@@ -461,6 +477,14 @@ letzten Zustellung stehen an der Karte.
|
|||||||
Fokusring, `prefers-reduced-motion` schaltet Animationen ab.
|
Fokusring, `prefers-reduced-motion` schaltet Animationen ab.
|
||||||
- **Deutschsprachig**, Datums- und Zahlenformate in `de-DE` mit fester Zeitzone
|
- **Deutschsprachig**, Datums- und Zahlenformate in `de-DE` mit fester Zeitzone
|
||||||
Europe/Berlin, damit Server- und Browser-Darstellung übereinstimmen.
|
Europe/Berlin, damit Server- und Browser-Darstellung übereinstimmen.
|
||||||
|
- **Als App installierbar.** Ein Web-Manifest mit eigenen Symbolen — auf dem Handy
|
||||||
|
„Zum Startbildschirm hinzufügen“, danach läuft Nebula ohne Adressleiste. Die
|
||||||
|
maskierbare Fassung hat mehr Rand, weil Android Symbole je nach Gerät rund, eckig
|
||||||
|
oder als Tropfen zuschneidet.
|
||||||
|
- **Änderungsverlauf** hinter der Versionsnummer in der Fußzeile: was neu ist, was besser
|
||||||
|
wurde und welche Fehler behoben sind. Die Liste steht in `src/lib/changelog.ts` und ist
|
||||||
|
die einzige Quelle — ein Test hält sie und `package.json` zusammen, weil genau diese
|
||||||
|
beiden Angaben hier schon einmal auseinandergelaufen sind.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-9
@@ -28,11 +28,6 @@ Die Sparkline gibt es auf Karte und Projektseite. Eine Heatmap über ein Jahr
|
|||||||
wäre die nächste Ausbaustufe — dafür braucht es allerdings mehr als eine
|
wäre die nächste Ausbaustufe — dafür braucht es allerdings mehr als eine
|
||||||
Seite Commits, also Paginierung oder die Statistik-Endpunkte.
|
Seite Commits, also Paginierung oder die Statistik-Endpunkte.
|
||||||
|
|
||||||
### 2.2 Kanban mit freier Reihenfolge `M`
|
|
||||||
|
|
||||||
Heute folgt die Reihenfolge innerhalb einer Spalte der gewählten Sortierung.
|
|
||||||
Frei sortieren hieße, eine Position je Projekt zu speichern.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3 · Größere Brocken
|
## 3 · Größere Brocken
|
||||||
@@ -42,7 +37,7 @@ Frei sortieren hieße, eine Position je Projekt zu speichern.
|
|||||||
Heute setzt ein Administrator das Passwort neu. Selbstbedienung braucht SMTP
|
Heute setzt ein Administrator das Passwort neu. Selbstbedienung braucht SMTP
|
||||||
und damit eine weitere Konfiguration — lohnt erst im Mehrbenutzerbetrieb.
|
und damit eine weitere Konfiguration — lohnt erst im Mehrbenutzerbetrieb.
|
||||||
|
|
||||||
### 3.4 Mehrsprachigkeit `L`
|
### 3.2 Mehrsprachigkeit `L`
|
||||||
|
|
||||||
Die Oberfläche ist durchgängig deutsch verdrahtet. Englisch dazu wäre eine
|
Die Oberfläche ist durchgängig deutsch verdrahtet. Englisch dazu wäre eine
|
||||||
Voraussetzung, falls du das Projekt veröffentlichen willst.
|
Voraussetzung, falls du das Projekt veröffentlichen willst.
|
||||||
@@ -54,14 +49,11 @@ Voraussetzung, falls du das Projekt veröffentlichen willst.
|
|||||||
| Idee | Aufwand | Nutzen |
|
| Idee | Aufwand | Nutzen |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste |
|
| Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste |
|
||||||
| Mehrfachauswahl im Board | `S` | Status oder Tags für mehrere auf einmal |
|
|
||||||
| Wiederkehrende Aufgaben | `M` | Etwa „jeden Montag Abhängigkeiten prüfen" |
|
| Wiederkehrende Aufgaben | `M` | Etwa „jeden Montag Abhängigkeiten prüfen" |
|
||||||
| Projektvorlagen | `S` | Neues Projekt mit vorgefüllten Tags und Notizen |
|
| Projektvorlagen | `S` | Neues Projekt mit vorgefüllten Tags und Notizen |
|
||||||
| Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel |
|
| Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel |
|
||||||
| Eigenes Logo und Farbe | `S` | Ergänzt `APP_NAME` |
|
| Eigenes Logo und Farbe | `S` | Ergänzt `APP_NAME` |
|
||||||
| PWA-Manifest | `S` | Auf dem Handy als App installierbar |
|
|
||||||
| Barrierefreiheits-Durchsicht | `M` | Fokusreihenfolge, Screenreader, Kontraste |
|
| Barrierefreiheits-Durchsicht | `M` | Fokusreihenfolge, Screenreader, Kontraste |
|
||||||
| `prisma.config.ts` | `S` | Beseitigt die Deprecation-Warnung vor Prisma 7. Achtung: danach lädt Prisma die `.env` nicht mehr von selbst |
|
|
||||||
| Audit-Log für Adminaktionen | `M` | Nachvollziehbar, wer Konten geändert hat |
|
| Audit-Log für Adminaktionen | `M` | Nachvollziehbar, wer Konten geändert hat |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -101,6 +93,7 @@ Damit klar ist, was **nicht** kommt und warum:
|
|||||||
| **v0.8.0** Stillstehende Projekte melden, Schwelle im Adminbereich | ✓ |
|
| **v0.8.0** Stillstehende Projekte melden, Schwelle im Adminbereich | ✓ |
|
||||||
| **v0.9.0** Schnellsuche per `Strg+K`, Aufgaben aus Git-Issues | ✓ |
|
| **v0.9.0** Schnellsuche per `Strg+K`, Aufgaben aus Git-Issues | ✓ |
|
||||||
| **v1.0.0** Aktivitätsverlauf je Projekt, Zwei-Faktor per TOTP, mehrere Repositories je Projekt | ✓ |
|
| **v1.0.0** Aktivitätsverlauf je Projekt, Zwei-Faktor per TOTP, mehrere Repositories je Projekt | ✓ |
|
||||||
|
| **v1.1.0** Freie Reihenfolge im Brett, Mehrfachauswahl, installierbar als App, Änderungsverlauf | ✓ |
|
||||||
|
|
||||||
Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur
|
Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur
|
||||||
beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben
|
beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben
|
||||||
|
|||||||
@@ -279,6 +279,101 @@ test("Mehrere Repositories je Projekt", async ({ request, playwright }) => {
|
|||||||
expect(zumSchluss.repositories[0].repoName).toBe("swr");
|
expect(zumSchluss.repositories[0].repoName).toBe("swr");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Brett: Reihenfolge und Mehrfachauswahl", async ({ request, playwright }) => {
|
||||||
|
await login(request, ADMIN);
|
||||||
|
|
||||||
|
const angelegt = [];
|
||||||
|
for (const name of ["Brett A", "Brett B", "Brett C"]) {
|
||||||
|
const response = await request.post("/api/projects", { data: { name, status: "IDEA" } });
|
||||||
|
angelegt.push((await response.json()).project);
|
||||||
|
}
|
||||||
|
// Neue Projekte haengen sich hinten an ihre Spalte an – fortlaufend, nicht
|
||||||
|
// alle auf denselben Platz. Absolut zaehlen laesst sich nicht: in der
|
||||||
|
// Spalte stehen schon Projekte aus frueheren Tests.
|
||||||
|
const [a, b, c] = angelegt.map((p) => p.position);
|
||||||
|
expect([b, c]).toEqual([a + 1, a + 2]);
|
||||||
|
|
||||||
|
const ids = angelegt.map((p) => p.id);
|
||||||
|
const vorher = angelegt[0].updatedAt;
|
||||||
|
|
||||||
|
// Umdrehen und dabei eines in eine andere Spalte schieben.
|
||||||
|
const gedreht = [ids[2], ids[1]];
|
||||||
|
const reorder = await request.patch("/api/projects/reorder", {
|
||||||
|
data: { columns: { IDEA: gedreht, IN_DEVELOPMENT: [ids[0]] } },
|
||||||
|
});
|
||||||
|
expect(reorder.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const alle = (await (await request.get("/api/projects")).json()).projects;
|
||||||
|
const finde = (id: string) => alle.find((p: { id: string }) => p.id === id);
|
||||||
|
expect(finde(ids[2]).position).toBe(0);
|
||||||
|
expect(finde(ids[1]).position).toBe(1);
|
||||||
|
expect(finde(ids[0]).status).toBe("IN_DEVELOPMENT");
|
||||||
|
|
||||||
|
// Reines Umsortieren darf das Aenderungsdatum nicht anfassen – sonst
|
||||||
|
// schoebe ein Aufraeumen im Brett alles in "Zuletzt geaendert" nach oben.
|
||||||
|
expect(new Date(finde(ids[1]).updatedAt).getTime()).toBe(
|
||||||
|
new Date(angelegt[1].updatedAt).getTime(),
|
||||||
|
);
|
||||||
|
// Das verschobene dagegen hat sich wirklich geaendert.
|
||||||
|
expect(new Date(finde(ids[0]).updatedAt).getTime()).toBeGreaterThan(
|
||||||
|
new Date(vorher).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mehrfachauswahl: Tags ergaenzen und wieder entfernen.
|
||||||
|
const ergaenzt = await request.post("/api/projects/bulk", {
|
||||||
|
data: { ids, action: "tags", tags: ["Sammel", "test"], tagMode: "add" },
|
||||||
|
});
|
||||||
|
expect((await ergaenzt.json()).changed).toBe(3);
|
||||||
|
const mitTags = (await (await request.get("/api/projects")).json()).projects;
|
||||||
|
// Kleingeschrieben abgelegt, wie ueberall.
|
||||||
|
expect(mitTags.find((p: { id: string }) => p.id === ids[0]).tags).toContain("sammel");
|
||||||
|
|
||||||
|
const entfernt = await request.post("/api/projects/bulk", {
|
||||||
|
data: { ids, action: "tags", tags: ["sammel"], tagMode: "remove" },
|
||||||
|
});
|
||||||
|
expect((await entfernt.json()).changed).toBe(3);
|
||||||
|
|
||||||
|
// Status fuer mehrere: die Verschobenen landen hintereinander, nicht
|
||||||
|
// uebereinander auf derselben Position.
|
||||||
|
const verschoben = await request.post("/api/projects/bulk", {
|
||||||
|
data: { ids, action: "status", status: "PLANNING" },
|
||||||
|
});
|
||||||
|
expect((await verschoben.json()).changed).toBe(3);
|
||||||
|
const geplant = (await (await request.get("/api/projects?status=PLANNING")).json()).projects;
|
||||||
|
const positionen = geplant.map((p: { position: number }) => p.position).sort();
|
||||||
|
expect(new Set(positionen).size).toBe(positionen.length);
|
||||||
|
|
||||||
|
// Wer schon so steht, wird nicht noch einmal angefasst.
|
||||||
|
const nochmal = await request.post("/api/projects/bulk", {
|
||||||
|
data: { ids, action: "status", status: "PLANNING" },
|
||||||
|
});
|
||||||
|
expect((await nochmal.json()).changed).toBe(0);
|
||||||
|
|
||||||
|
// Fremde IDs fallen still unter den Tisch, statt etwas anzurichten.
|
||||||
|
const fremd = await playwright.request.newContext({
|
||||||
|
baseURL: test.info().project.use.baseURL,
|
||||||
|
});
|
||||||
|
await login(fremd, ZWEITER);
|
||||||
|
const untergeschoben = await fremd.post("/api/projects/bulk", {
|
||||||
|
data: { ids, action: "delete" },
|
||||||
|
});
|
||||||
|
expect((await untergeschoben.json()).changed).toBe(0);
|
||||||
|
const uebrig = (await (await request.get("/api/projects")).json()).projects;
|
||||||
|
expect(uebrig.filter((p: { id: string }) => ids.includes(p.id))).toHaveLength(3);
|
||||||
|
|
||||||
|
const fremdSortieren = await fremd.patch("/api/projects/reorder", {
|
||||||
|
data: { columns: { DONE: ids } },
|
||||||
|
});
|
||||||
|
expect((await fremdSortieren.json()).updated).toBe(0);
|
||||||
|
await fremd.dispose();
|
||||||
|
|
||||||
|
// Und zum Schluss die eigenen wirklich loeschen.
|
||||||
|
const geloescht = await request.post("/api/projects/bulk", {
|
||||||
|
data: { ids, action: "delete" },
|
||||||
|
});
|
||||||
|
expect((await geloescht.json()).changed).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async ({ request }) => {
|
test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async ({ request }) => {
|
||||||
await login(request, ADMIN);
|
await login(request, ADMIN);
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,17 @@ test("Aufgaben anlegen und per Ziehen verschieben", async ({ page }) => {
|
|||||||
// dnd-kit wertet die Bewegung in einem eigenen Frame aus. Ohne diese
|
// dnd-kit wertet die Bewegung in einem eigenen Frame aus. Ohne diese
|
||||||
// Pause faende das Loslassen noch kein Ziel und die Karte spraenge zurueck.
|
// Pause faende das Loslassen noch kein Ziel und die Karte spraenge zurueck.
|
||||||
await page.waitForTimeout(250);
|
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 page.mouse.up();
|
||||||
|
|
||||||
await expect(inArbeit.getByText("Erste Aufgabe")).toBeVisible();
|
await expect(inArbeit.getByText("Erste Aufgabe")).toBeVisible();
|
||||||
|
expect((await gespeichert).ok()).toBeTruthy();
|
||||||
|
|
||||||
// Und der Server hat es auch so gespeichert.
|
// Und der Server hat es auch so gespeichert.
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -119,6 +127,73 @@ test("Schnellsuche findet Projekt und Notiz", async ({ page }) => {
|
|||||||
await expect(page.getByRole("dialog", { name: "Schnellsuche" })).toBeHidden();
|
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 }) => {
|
test("Abmelden fuehrt zurueck zur Anmeldung", async ({ page }) => {
|
||||||
await page.getByRole("button", { name: /Benutzermenue/ }).click();
|
await page.getByRole("button", { name: /Benutzermenue/ }).click();
|
||||||
await page.getByRole("menuitem", { name: "Abmelden" }).click();
|
await page.getByRole("menuitem", { name: "Abmelden" }).click();
|
||||||
|
|||||||
Generated
+20
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "nebula",
|
"name": "nebula",
|
||||||
"version": "0.9.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nebula",
|
"name": "nebula",
|
||||||
"version": "0.9.0",
|
"version": "1.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.1.5",
|
"@types/react": "^19.1.5",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"prisma": "^6.8.2",
|
"prisma": "^6.8.2",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
@@ -2574,6 +2575,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12/node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
@@ -2866,10 +2880,10 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "17.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|||||||
+2
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nebula",
|
"name": "nebula",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)",
|
"description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,9 +30,6 @@
|
|||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:all": "npm test && npm run test:e2e"
|
"test:all": "npm test && npm run test:e2e"
|
||||||
},
|
},
|
||||||
"prisma": {
|
|
||||||
"seed": "tsx prisma/seed.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
@@ -59,6 +56,7 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.1.5",
|
"@types/react": "^19.1.5",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"prisma": "^6.8.2",
|
"prisma": "^6.8.2",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { config as loadEnv } from "dotenv";
|
||||||
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguration der Prisma-CLI.
|
||||||
|
*
|
||||||
|
* Loest den Abschnitt `prisma` in der package.json ab, der mit Prisma 7
|
||||||
|
* wegfaellt.
|
||||||
|
*
|
||||||
|
* **Der Aufruf von dotenv ist nicht optional.** Sobald diese Datei
|
||||||
|
* existiert, liest Prisma die .env nicht mehr von selbst ein – ohne die
|
||||||
|
* folgenden Zeilen stuende `migrate deploy` plotzlich ohne DATABASE_URL da,
|
||||||
|
* und zwar mit einer Fehlermeldung, die nicht darauf hindeutet. Erst .env,
|
||||||
|
* dann .env.local: eine oertliche Abweichung soll gewinnen.
|
||||||
|
*/
|
||||||
|
loadEnv({ path: ".env", quiet: true });
|
||||||
|
loadEnv({ path: ".env.local", override: true, quiet: true });
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
seed: "tsx prisma/seed.ts",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { NOTIFICATION_EVENTS } from "@/lib/notify/types";
|
import { NOTIFICATION_EVENTS } from "@/lib/notify/types";
|
||||||
import { notificationTargetCreateSchema } from "./validation";
|
import {
|
||||||
|
notificationTargetCreateSchema,
|
||||||
|
projectBulkSchema,
|
||||||
|
projectReorderSchema,
|
||||||
|
} from "./validation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dieser Test entstand aus einem Fehler: beim Ergaenzen eines neuen
|
* Dieser Test entstand aus einem Fehler: beim Ergaenzen eines neuen
|
||||||
@@ -70,3 +74,60 @@ describe("notificationTargetCreateSchema", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("projectReorderSchema", () => {
|
||||||
|
it("nimmt Spalten mit Listen an", () => {
|
||||||
|
const parsed = projectReorderSchema.safeParse({
|
||||||
|
columns: { IDEA: ["a", "b"], DONE: [] },
|
||||||
|
});
|
||||||
|
expect(parsed.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weist unbekannte Spalten ab", () => {
|
||||||
|
expect(
|
||||||
|
projectReorderSchema.safeParse({ columns: { PHANTASIE: ["a"] } }).success,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weist einen leeren Aufruf ab, statt ihn wirkungslos durchzuwinken", () => {
|
||||||
|
expect(projectReorderSchema.safeParse({ columns: {} }).success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("projectBulkSchema", () => {
|
||||||
|
const ids = ["a", "b"];
|
||||||
|
|
||||||
|
it("verlangt zu jeder Aktion die noetige Angabe", () => {
|
||||||
|
// Ohne diese Pruefungen kaeme die Aktion durch und taete nichts – der
|
||||||
|
// schlimmste Fall, weil niemand merkt, dass nichts passiert ist.
|
||||||
|
expect(projectBulkSchema.safeParse({ ids, action: "status" }).success).toBe(false);
|
||||||
|
expect(projectBulkSchema.safeParse({ ids, action: "tags" }).success).toBe(false);
|
||||||
|
expect(projectBulkSchema.safeParse({ ids, action: "tags", tags: [] }).success).toBe(false);
|
||||||
|
expect(projectBulkSchema.safeParse({ ids, action: "favorite" }).success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("laesst vollstaendige Aktionen durch", () => {
|
||||||
|
expect(
|
||||||
|
projectBulkSchema.safeParse({ ids, action: "status", status: "DONE" }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
projectBulkSchema.safeParse({ ids, action: "tags", tags: ["archiv"] }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
projectBulkSchema.safeParse({ ids, action: "favorite", favorite: false }).success,
|
||||||
|
).toBe(true);
|
||||||
|
// Loeschen braucht nichts ausser den IDs.
|
||||||
|
expect(projectBulkSchema.safeParse({ ids, action: "delete" }).success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ergaenzt standardmaessig, statt zu entfernen", () => {
|
||||||
|
const parsed = projectBulkSchema.parse({ ids, action: "tags", tags: ["x"] });
|
||||||
|
expect(parsed.tagMode).toBe("add");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("braucht mindestens eine ID und deckelt die Menge", () => {
|
||||||
|
expect(projectBulkSchema.safeParse({ ids: [], action: "delete" }).success).toBe(false);
|
||||||
|
const zuViele = Array.from({ length: 201 }, (_, index) => `id-${index}`);
|
||||||
|
expect(projectBulkSchema.safeParse({ ids: zuViele, action: "delete" }).success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user