Moderne Testing-Praxis – React Todo-App mit Vite und Playwright
In der modernen Frontend-Entwicklung ist End-to-End-Testing ein Muss. Mit diesem Tutorial baust du eine React Todo-App mit Vite und testest sie komfortabel mit Playwright – in mehreren Browsern, im UI-Debugger oder als HTML-Report!
Inhaltsverzeichnis
Abschnitt betitelt „Inhaltsverzeichnis“- Einführung
- Projekt-Setup mit Vite
- React-Komponenten der Todo-App
- Stil hinzufügen
- Playwright einrichten
- E2E-Tests mit Playwright
- Playwright Report & UI nutzen
- Best Practices
- Fazit
Einführung
Abschnitt betitelt „Einführung“Warum Vite? Sehr schnelle Entwicklungsstarts, Hot-Reloading, einfache Konfiguration.
Warum Playwright? Modernes Cross-Browser E2E-Testing, parallele Ausführung, tolle Debugging-Oberfläche, super für CI/CD!
Projekt-Setup mit Vite
Abschnitt betitelt „Projekt-Setup mit Vite“Projektstruktur anlegen:
npm create vite@latest react-todo-playwright -- --template reactcd react-todo-playwrightnpm installStruktur ergänzen:
mkdir src/componentsmkdir src/stylestouch src/styles/App.css src/components/TodoForm.jsx src/components/TodoItem.jsxTipp: Die Datei
src/index.csskannst du leeren oder löschen.
React-Komponenten der Todo-App
Abschnitt betitelt „React-Komponenten der Todo-App“src/App.jsx
Abschnitt betitelt „src/App.jsx“import { useState, useEffect } from "react";import TodoForm from "./components/TodoForm";import TodoItem from "./components/TodoItem";import "./styles/App.css";
function App() { const [todos, setTodos] = useState(() => { const savedTodos = localStorage.getItem("todos"); return savedTodos ? JSON.parse(savedTodos) : []; });
useEffect(() => { localStorage.setItem("todos", JSON.stringify(todos)); }, [todos]);
const addTodo = (text) => { if (text.trim() !== "") { setTodos([ ...todos, { id: Date.now(), text, completed: false, }, ]); } };
const toggleTodo = (id) => { setTodos( todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); };
const deleteTodo = (id) => { setTodos(todos.filter((todo) => todo.id !== id)); };
return ( <div className="app-container" data-testid="app-container"> <h1>React Todo App</h1> <TodoForm onAddTodo={addTodo} />
<div className="todo-list" data-testid="todo-list"> {todos.length === 0 ? ( <p className="empty-message" data-testid="empty-message"> Keine Todos vorhanden. Füge welche hinzu! </p> ) : ( todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} onDelete={deleteTodo} /> )) )} </div> </div> );}
export default App;src/components/TodoForm.jsx
Abschnitt betitelt „src/components/TodoForm.jsx“import { useState } from "react";
function TodoForm({ onAddTodo }) { const [text, setText] = useState("");
const handleSubmit = (e) => { e.preventDefault(); onAddTodo(text); setText(""); };
return ( <form onSubmit={handleSubmit} data-testid="todo-form"> <input type="text" value={text} onChange={(e) => setText(e.target.value)} placeholder="Was muss erledigt werden?" data-testid="todo-input" /> <button type="submit" data-testid="add-button"> Hinzufügen </button> </form> );}
export default TodoForm;src/components/TodoItem.jsx
Abschnitt betitelt „src/components/TodoItem.jsx“function TodoItem({ todo, onToggle, onDelete }) { return ( <div className={`todo-item ${todo.completed ? "completed" : ""}`} data-testid="todo-item" > <span onClick={() => onToggle(todo.id)} data-testid="todo-text" className="todo-text" > {todo.text} </span> <button onClick={() => onDelete(todo.id)} data-testid="delete-button" className="delete-button" > Löschen </button> </div> );}
export default TodoItem;Stil hinzufügen
Abschnitt betitelt „Stil hinzufügen“src/styles/App.css
Abschnitt betitelt „src/styles/App.css“* { box-sizing: border-box; margin: 0; padding: 0;}
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background-color: #f5f5f5;}
.app-container { max-width: 600px; margin: 0 auto; padding: 20px; background-color: white; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); border-radius: 8px; margin-top: 40px;}
h1 { text-align: center; margin-bottom: 20px; color: #2c3e50;}
form { display: flex; margin-bottom: 20px;}
input { flex: 1; padding: 10px; font-size: 16px; border: 1px solid #ddd; border-radius: 4px 0 0 4px; outline: none;}
button { padding: 10px 15px; background-color: #3498db; color: white; border: none; cursor: pointer; transition: background-color 0.3s;}
form button { border-radius: 0 4px 4px 0;}
button:hover { background-color: #2980b9;}
.todo-list { margin-top: 20px;}
.todo-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; margin-bottom: 10px; background-color: #f9f9f9; border-radius: 4px; transition: all 0.3s;}
.todo-text { cursor: pointer; flex: 1;}
.completed .todo-text { text-decoration: line-through; color: #888;}
.delete-button { background-color: #e74c3c; border-radius: 4px; padding: 5px 10px; font-size: 14px;}
.delete-button:hover { background-color: #c0392b;}
.empty-message { text-align: center; color: #7f8c8d; font-style: italic;}Playwright einrichten
Abschnitt betitelt „Playwright einrichten“Playwright installieren
Abschnitt betitelt „Playwright installieren“npm install --save-dev @playwright/testnpx playwright installKonfiguration (playwright.config.ts)
Abschnitt betitelt „Konfiguration (playwright.config.ts)“In deiner Projektwurzel (Datei anlegen oder überschreiben):
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ testDir: './tests', use: { baseURL: 'http://localhost:5173', trace: 'on-first-retry', }, reporter: [['html']], // <--- HTML-Report wird automatisch generiert projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ],});Package-Skripte ergänzen
Abschnitt betitelt „Package-Skripte ergänzen“Empfohlen im package.json:
"scripts": { "dev": "vite", "test:ui": "playwright test --ui", "test": "playwright test", "test:report": "playwright test --reporter=html", "show-report": "playwright show-report"}E2E-Tests mit Playwright
Abschnitt betitelt „E2E-Tests mit Playwright“Test-Ordner anlegen
Abschnitt betitelt „Test-Ordner anlegen“mkdir testsBeispiel: tests/todo.spec.ts
Abschnitt betitelt „Beispiel: tests/todo.spec.ts“import { test, expect } from "@playwright/test";
test.describe('Todo App', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await page.evaluate(() => localStorage.clear()); await page.reload(); });
test('zeigt die Überschrift korrekt an', async ({ page }) => { await expect(page.getByRole('heading', { level: 1 })).toHaveText('React Todo App'); });
test('zeigt anfangs eine leere Nachricht', async ({ page }) => { await expect(page.getByTestId('empty-message')).toBeVisible(); await expect(page.getByTestId('todo-item')).toHaveCount(0); });
test('kann ein neues Todo hinzufügen', async ({ page }) => { await page.getByTestId('todo-input').fill('Testtodo'); await page.getByTestId('add-button').click(); await expect(page.getByTestId('todo-item')).toHaveCount(1); await expect(page.getByTestId('empty-message')).toHaveCount(0); await expect(page.getByTestId('todo-text')).toHaveText('Testtodo'); });
test('kann mehrere Todos hinzufügen', async ({ page }) => { const todos = ['Erstes Todo', 'Zweites Todo', 'Drittes Todo']; for (const todo of todos) { await page.getByTestId('todo-input').fill(todo); await page.getByTestId('add-button').click(); } await expect(page.getByTestId('todo-item')).toHaveCount(3); for (let i = 0; i < todos.length; i++) { await expect(page.getByTestId('todo-text').nth(i)).toHaveText(todos[i]); } });
test('kann ein Todo als erledigt markieren und zurücksetzen', async ({ page }) => { await page.getByTestId('todo-input').fill('Erledigen'); await page.getByTestId('add-button').click(); const item = page.getByTestId('todo-item').first(); const text = page.getByTestId('todo-text').first();
await expect(item).not.toHaveClass(/completed/);
await text.click(); await expect(item).toHaveClass(/completed/);
await text.click(); await expect(item).not.toHaveClass(/completed/); });
test('kann ein Todo löschen', async ({ page }) => { await page.getByTestId('todo-input').fill('Löschen'); await page.getByTestId('add-button').click(); await expect(page.getByTestId('todo-item')).toHaveCount(1);
await page.getByTestId('delete-button').click(); await expect(page.getByTestId('todo-item')).toHaveCount(0); await expect(page.getByTestId('empty-message')).toBeVisible(); });
test('speichert Todos im localStorage', async ({ page }) => { await page.getByTestId('todo-input').fill('Persistenz!'); await page.getByTestId('add-button').click(); await expect(page.getByTestId('todo-item')).toHaveCount(1);
await page.reload();
await expect(page.getByTestId('todo-item')).toHaveCount(1); await expect(page.getByTestId('todo-text').first()).toHaveText('Persistenz!'); });
test('fügt keine leeren Todos hinzu (leer)', async ({ page }) => { await page.getByTestId('add-button').click(); await expect(page.getByTestId('todo-item')).toHaveCount(0); });
test('fügt keine leeren Todos hinzu (nur Leerzeichen)', async ({ page }) => { await page.getByTestId('todo-input').fill(' '); await page.getByTestId('add-button').click(); await expect(page.getByTestId('todo-item')).toHaveCount(0); });});Playwright Report und UI nutzen
Abschnitt betitelt „Playwright Report und UI nutzen“1. UI-Mode (empfohlen)
Abschnitt betitelt „1. UI-Mode (empfohlen)“Mit visuellem Debugging, Step-By-Step und Live-Ausführung:
npm run test:uioder
npx playwright test --uiHier kannst du Tests einzeln starten, debuggen, Fehler direkt nachvollziehen und sogar Snapshots der Steps sehen.
2. HTML-Report ansehen
Abschnitt betitelt „2. HTML-Report ansehen“Da im playwright.config.ts der HTML-Reporter schon gesetzt ist, erzeugt jeder Testlauf einen Report.
npm run test# odernpx playwright testZeige dann das Ergebnis an:
npm run show-report# odernpx playwright show-report→ Öffnet http://localhost:9323 im Browser.
Best Practices
Abschnitt betitelt „Best Practices“- Nutze ausschliesslich eigene Selektoren wie
data-testidfür stabile Tests - Mache keine Annahmen über die Reihenfolge der Todos – greife gezielt auf sie zu!
- Lass jeden Test unabhängig voneinander laufen (localStorage leeren, etc.)
- Nutze UI- und reporter-Features von Playwright zur Fehlersuche und für Team-Reports
Mit Vite, React und Playwright baust du dir eine moderne, zukunftssichere und blitzschnelle Entwicklungs- und Testumgebung.
- Baue eigene Features, füge systematische Playwright-Tests hinzu
- Vertraue auf hochwertige Reports bei jedem Push und Debugging-Komfort im UI-Mode
Viel Freude beim Entwickeln und Testen!