Zielgruppe: Entwickler & Maintenance
Schwierigkeit: ⭐ Easy bis ⭐⭐ Medium
Zeit: 10 Minuten Setup + Durchführung
Das Projekt nutzt 3 spezialisierte Test-Tools für unterschiedliche Szenarien:
| Tool | Typ | Ort | Geschwindigkeit | Browser | Wann nutzen |
|---|---|---|---|---|---|
| Vitest | Unit | *.spec.ts neben Komponente |
⚡⚡⚡ Sehr schnell | ❌ Nein | Funktions-Tests |
| Vitest | Store | *.svelte.spec.ts neben Store |
⚡⚡ Schnell | ❌ Nein | State & Reactivity |
| Playwright | E2E | e2e/*.test.ts |
🐢 Langsam | ✅ Ja | User-Journeys |
# Einmalig ausführen
pnpm run test:unit
# ODER: Mit Watch Mode (während Entwicklung)
pnpm run test:unit:watch
# ODER: Nur bestimmte Datei
pnpm run test:unit -- BoardModel.spec.tsOutput Beispiel:
✓ src/lib/classes/BoardModel.spec.ts (12 tests) 15ms
✓ src/lib/stores/kanbanStore.svelte.spec.ts (8 tests) 22ms
✓ src/routes/cardsboard/Card.svelte.spec.ts (5 tests) 8ms
Test Files 3 passed (3)
Tests 25 passed (25)
Duration 1.93s
# Nur E2E Tests
pnpm run test:e2e
# ODER: Mit UI-Debugger
pnpm run test:e2e -- --ui
# ODER: Mit headed Browser (sichtbar)
pnpm run test:e2e -- --headedOutput Beispiel:
Running 1 test using 1 worker
✓ e2e/demo.test.ts:3:1 › home page has expected h1 (421ms)
1 passed (29.8s)
# Unit + E2E Tests
pnpm run test
# Aktuell: 45 Unit Tests + 1 E2E Test = 46 TestsDateistruktur:
src/lib/classes/
├── BoardModel.ts
└── BoardModel.spec.ts ← Test sitzt daneben!
Beispiel: Card-Klasse testen
// src/lib/classes/BoardModel.spec.ts
import { describe, it, expect } from 'vitest';
import { Card, Column, Board } from './BoardModel';
describe('Card Operations', () => {
it('erstellt Karte mit eindeutiger ID', () => {
const card = new Card({ heading: 'Test Karte' });
expect(card.id).toBeDefined();
expect(card.id.length).toBeGreaterThan(0);
expect(card.heading).toBe('Test Karte');
});
it('aktualisiert Karten-Eigenschaften', () => {
const card = new Card({ heading: 'Alt' });
card.update({ heading: 'Neu', color: 'red' });
expect(card.heading).toBe('Neu');
expect(card.color).toBe('red');
});
it('verwaltet Kommentare korrekt', () => {
const card = new Card({ heading: 'Test' });
card.addComment('Wichtig!', 'npub123');
expect(card.comments).toHaveLength(1);
expect(card.comments[0].text).toBe('Wichtig!');
const commentId = card.comments[0].id;
card.deleteComment(commentId);
expect(card.comments).toHaveLength(0);
});
it('serialisiert korrekt für KI-Kontext', () => {
const card = new Card({
heading: 'Aufgabe',
content: 'Beschreibung',
labels: ['bug', 'urgent']
});
const context = card.getContextData();
expect(context.id).toBeDefined();
expect(context.heading).toBe('Aufgabe');
expect(context.labels).toEqual(['bug', 'urgent']);
expect(context.publishState).toBe('draft');
});
});
describe('Board Operations', () => {
it('verwaltet Spalten und Karten hierarchisch', () => {
const board = new Board({ name: 'Test Board' });
const col1 = board.addColumn({ name: 'To Do' });
const col2 = board.addColumn({ name: 'Done' });
const card = col1.addCard({ heading: 'Task' });
expect(board.columns).toHaveLength(2);
expect(col1.cards).toHaveLength(1);
expect(card.heading).toBe('Task');
});
it('verschiebt Karten zwischen Spalten', () => {
const board = new Board({ name: 'Test' });
const col1 = board.addColumn({ name: 'A' });
const col2 = board.addColumn({ name: 'B' });
const card = col1.addCard({ heading: 'Task' });
const cardId = card.id;
board.moveCard(cardId, col1.id, col2.id);
expect(col1.cards).toHaveLength(0);
expect(col2.cards).toHaveLength(1);
expect(col2.findCard(cardId)).toBeDefined();
});
it('findet Karten im gesamten Board', () => {
const board = new Board({ name: 'Test' });
const col = board.addColumn({ name: 'To Do' });
const card = col.addCard({ heading: 'Task' });
const found = board.findCardAndColumn(card.id);
expect(found).not.toBeNull();
expect(found?.card.heading).toBe('Task');
expect(found?.column.name).toBe('To Do');
});
});Dateistruktur:
src/lib/stores/
├── kanbanStore.svelte.ts
└── kanbanStore.svelte.spec.ts ← Test neben Store!
Beispiel: BoardStore Reaktivität testen
// src/lib/stores/kanbanStore.svelte.spec.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { boardStore } from './kanbanStore.svelte';
describe('BoardStore Reactivity', () => {
beforeEach(() => {
// Setup vor jedem Test
localStorage.clear();
boardStore.reset();
});
afterEach(() => {
localStorage.clear();
});
it('erstellt neue Karten und triggert $derived Update', () => {
const colId = boardStore.addColumn({ name: 'To Do' });
// Initial state
expect(boardStore.uiData.columns).toHaveLength(1);
// createCard() ruft triggerUpdate() auf
boardStore.createCard(colId, 'Test Card');
// $derived.by wurde getriggert → uiData neu berechnet
expect(boardStore.uiData.columns[0].cards).toHaveLength(1);
expect(boardStore.uiData.columns[0].cards[0].title).toBe('Test Card');
});
it('aktualisiert localStorage synchron nach triggerUpdate()', () => {
const colId = boardStore.addColumn({ name: 'Done' });
boardStore.createCard(colId, 'Important Task');
// localStorage wurde synchron aktualisiert (in triggerUpdate())
const stored = JSON.parse(localStorage.getItem('kanban-board-data') || '{}');
expect(stored.columns).toHaveLength(1);
expect(stored.columns[0].cards).toHaveLength(1);
expect(stored.columns[0].cards[0].heading).toBe('Important Task');
});
it('behält Spalten-Reihenfolge nach Drag-and-Drop bei', () => {
boardStore.addColumn({ name: 'A' });
boardStore.addColumn({ name: 'B' });
boardStore.addColumn({ name: 'C' });
// Simuliere Drag-and-Drop: C → Position 0
const reorderedColumns = [
boardStore.uiData.columns[2], // C
boardStore.uiData.columns[0], // A
boardStore.uiData.columns[1] // B
];
boardStore.syncBoardState(reorderedColumns);
// Neue Reihenfolge sollte C, A, B sein
expect(boardStore.uiData.columns[0].name).toBe('C');
expect(boardStore.uiData.columns[1].name).toBe('A');
expect(boardStore.uiData.columns[2].name).toBe('B');
});
it('lädt Daten aus localStorage beim Init', () => {
// Setze Test-Daten in localStorage
const testBoard = {
id: 'board-1',
name: 'Saved Board',
columns: [
{
id: 'col-1',
name: 'To Do',
cards: [{ id: 'card-1', heading: 'Task' }]
}
]
};
localStorage.setItem('kanban-board-data', JSON.stringify(testBoard));
// Neuer Store lädt Daten
const newStore = new BoardStore();
expect(newStore.boardMeta.name).toBe('Saved Board');
expect(newStore.uiData.columns).toHaveLength(1);
expect(newStore.uiData.columns[0].cards).toHaveLength(1);
});
});
describe('BoardStore Card Editing', () => {
beforeEach(() => {
localStorage.clear();
boardStore.reset();
});
it('editiert Kartenmetriken und persistiert sofort', () => {
const colId = boardStore.addColumn({ name: 'Backlog' });
const cardId = boardStore.createCard(colId, 'Original');
boardStore.editCard(cardId, {
heading: 'Updated',
color: 'blue'
});
const card = boardStore.findCard(cardId);
expect(card?.heading).toBe('Updated');
expect(card?.color).toBe('blue');
// Verifikation: localStorage wurde aktualisiert
const stored = JSON.parse(localStorage.getItem('kanban-board-data') || '{}');
expect(stored.columns[0].cards[0].heading).toBe('Updated');
});
});Dateistruktur:
e2e/
├── demo.test.ts ← Existing
└── board-operations.test.ts ← New
Beispiel: Board-Operationen E2E testen
// e2e/board-operations.test.ts
import { test, expect } from '@playwright/test';
test.describe('Board User Journey', () => {
test.beforeEach(async ({ page }) => {
// Navigiere zur App
await page.goto('/cardsboard');
// Warte bis Seite geladen ist
await expect(page.getByRole('heading', { name: /Kanban/i })).toBeVisible();
});
test('user can create board and add cards', async ({ page }) => {
// 1. Navigiere zum Hauptbereich
await expect(page.getByRole('button', { name: /Neues Board/ })).toBeVisible();
// 2. Erstelle neue Spalte
await page.getByRole('button', { name: /Spalte hinzufügen/ }).click();
await page.getByPlaceholder('Spalten-Name').fill('To Do');
await page.getByRole('button', { name: /Erstellen/ }).click();
// 3. Verifikation: Spalte wurde erstellt
await expect(page.getByText('To Do')).toBeVisible();
// 4. Füge Karte hinzu
await page.getByRole('button', { name: /Karte hinzufügen/ }).click();
await page.getByPlaceholder('Karten-Titel').fill('My First Task');
await page.getByRole('button', { name: /Speichern/ }).click();
// 5. Verifikation: Karte wurde erstellt
await expect(page.getByText('My First Task')).toBeVisible();
});
test('user can drag and drop cards between columns', async ({ page }) => {
// Setup: Erstelle 2 Spalten
await page.getByRole('button', { name: /Spalte hinzufügen/ }).click();
await page.getByPlaceholder('Spalten-Name').fill('To Do');
await page.getByRole('button', { name: /Erstellen/ }).click();
await page.getByRole('button', { name: /Spalte hinzufügen/ }).click();
await page.getByPlaceholder('Spalten-Name').fill('Done');
await page.getByRole('button', { name: /Erstellen/ }).click();
// Erstelle Karte in To Do
const toDoSection = page.locator('text=To Do').first();
await toDoSection.getByRole('button', { name: /Karte hinzufügen/ }).click();
await page.getByPlaceholder('Karten-Titel').fill('Drag Me');
await page.getByRole('button', { name: /Speichern/ }).click();
// Drag Card von To Do nach Done
const card = page.getByText('Drag Me');
const doneSection = page.locator('text=Done');
await card.dragTo(doneSection);
// Verifikation: Karte ist nun in Done
await expect(doneSection.getByText('Drag Me')).toBeVisible();
});
test('user can edit card properties via modal', async ({ page }) => {
// Setup
await page.getByRole('button', { name: /Spalte hinzufügen/ }).click();
await page.getByPlaceholder('Spalten-Name').fill('Backlog');
await page.getByRole('button', { name: /Erstellen/ }).click();
// Erstelle Karte
await page.getByRole('button', { name: /Karte hinzufügen/ }).click();
await page.getByPlaceholder('Karten-Titel').fill('Original Title');
await page.getByRole('button', { name: /Speichern/ }).click();
// Öffne Card Modal
await page.getByText('Original Title').click();
// Editiere Titel
await page.getByPlaceholder('Titel').fill('Updated Title');
await page.getByRole('button', { name: /Speichern/ }).click();
// Verifikation: Titel wurde aktualisiert
await expect(page.getByText('Updated Title')).toBeVisible();
await expect(page.getByText('Original Title')).not.toBeVisible();
});
});src/
├── lib/
│ ├── classes/
│ │ ├── BoardModel.ts
│ │ └── BoardModel.spec.ts ← Unit Tests (Klassen)
│ ├── stores/
│ │ ├── kanbanStore.svelte.ts
│ │ ├── kanbanStore.svelte.spec.ts ← Store Tests (Reactivity)
│ │ ├── authStore.svelte.ts
│ │ └── authStore.svelte.spec.ts
│ └── utils/
│ ├── mergeEngine.ts
│ └── mergeEngine.spec.ts ← Utility Tests
│
├── routes/
│ ├── cardsboard/
│ │ ├── Card.svelte
│ │ ├── Card.svelte.spec.ts ← Component Tests
│ │ └── ...
│ └── page.svelte.spec.ts
│
e2e/
├── demo.test.ts ← E2E Tests
├── board-operations.test.ts
└── auth-flow.test.ts
// ✅ Tests neben Code
src/lib/classes/BoardModel.ts
src/lib/classes/BoardModel.spec.ts // ← Hier!
// ✅ Aussagekräftige Test-Namen
it('creates card with unique ID and default draft state', () => { ... });
// ✅ Setup/Teardown nutzen
beforeEach(() => {
localStorage.clear();
boardStore.reset();
});
// ✅ Ein Konzept pro Test
it('serializes card to context data', () => {
const card = new Card({ heading: 'Test' });
const context = card.getContextData();
expect(context.id).toBeDefined();
});
// ✅ Arrange-Act-Assert Pattern
it('moves card between columns', () => {
// Arrange
const board = new Board({ name: 'Test' });
const colA = board.addColumn({ name: 'A' });
const colB = board.addColumn({ name: 'B' });
const card = colA.addCard({ heading: 'Task' });
// Act
board.moveCard(card.id, colA.id, colB.id);
// Assert
expect(colB.findCard(card.id)).toBeDefined();
expect(colA.findCard(card.id)).toBeUndefined();
});// ❌ Tests zu weit weg vom Code
src/lib/classes/BoardModel.ts
tests/__tests__/BoardModel.test.ts // ← Nicht hier!
// ❌ Unklare Test-Namen
it('works', () => { ... });
// ❌ Tests mit abhängig voneinander
test1: create card
test2: assumes card from test1 exists // ❌ Abhängigkeit!
// ❌ Mehrere Konzepte pro Test
it('creates card and updates and serializes', () => { ... });
// ❌ Ohne Setup/Teardown
it('test', () => {
// Nächster Test könnte beeinträchtigt sein!
});- Was: Einzelne Funktionen, Klassen-Methoden
- Wo:
*.spec.tsneben der Komponente - Geschwindigkeit: ⚡⚡⚡ Sehr schnell (< 1s)
- Beispiel:
Card.addComment(),Board.moveCard()
- Was:
$stateReaktivität,$derivedNeuberechnungen,triggerUpdate()Side-Effects - Wo:
*.svelte.spec.tsneben dem Store - Geschwindigkeit: ⚡⚡ Schnell (1-5s)
- Beispiel:
boardStore.createCard()aktualisiert localStorage + uiData
- Was: Komplette User-Journeys im Browser
- Wo:
e2e/*.test.ts - Geschwindigkeit: 🐢 Langsam (30-60s)
- Beispiel: "Nutzer erstellt Board → Fügt Karte hinzu → Speichert"
// ❌ FALSCH
import Card from './Card.svelte';
// ✅ RICHTIG
import Card from './Card.svelte';
import { render } from '@testing-library/svelte';// ❌ FALSCH - keine triggerUpdate() Mutation
board.addColumn({ name: 'New' });
expect(store.uiData).toHaveLength(2);
// ✅ RICHTIG - nutze Store-Methode
boardStore.addColumn({ name: 'New' });
expect(boardStore.uiData.columns).toHaveLength(2);// ❌ FALSCH - Tests beeinflussen sich gegenseitig
it('test 1', () => {
localStorage.setItem('data', 'test1');
});
it('test 2', () => {
// localStorage hat immer noch 'test1'!
});
// ✅ RICHTIG - Cleanup nach jedem Test
afterEach(() => {
localStorage.clear();
});beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
});import { vi } from 'vitest';
const mockNDK = {
publish: vi.fn().mockResolvedValue(true),
fetchEvent: vi.fn()
};const mockNostr = {
getPublicKey: vi.fn().mockResolvedValue('npub1...'),
signEvent: vi.fn()
};
vi.stubGlobal('nostr', mockNostr);pnpm run test:unit:watch
# Tests laufen automatisch wenn du Code speicherstpnpm run test:unit -- BoardModel.spec.tspnpm run test:e2e -- --headed
# Browser bleibt offen zum manuellen Debugging- AGENTS.md — Test-Spezifikation & Patterns
- docs/COLLABORATION/ROADMAP.md — Test-Milestones
- docs/TESTSUITE/STATUS.md — Aktueller Test-Status
Test-Strategien des Projekts:
-
Unit Tests → Klassen & Funktionen testen
pnpm run test:unit
-
Store Tests → Reaktivität & Persistence testen
pnpm run test:unit # Same command! -
E2E Tests → User-Journeys im Browser testen
pnpm run test:e2e
Alle zusammen:
pnpm run test # Unit + E2EVersion: 4.0 (Modern Test Strategy)
Letztes Update: 31. Oktober 2025
Status: 🟢 Production-Ready
Siehe auch: docs/TESTSUITE/STATUS.md für Überblick