Playwright hat sich 2026 als der De-facto-Standard für End-to-End-Tests im JavaScript-Ökosystem etabliert.
Ob React, Vue, Next.js oder SvelteKit —
Playwright läuft cross-browser (Chromium, Firefox, WebKit), unterstützt TypeScript nativ
und liefert mit dem Trace Viewer ein unvergleichliches Debugging-Erlebnis.
Claude Code versteht die Playwright-API vollständig und generiert wartbare, produktionsreife Tests
in wenigen Minuten.
Kurzfassung: Dieser Guide zeigt dir sechs Kernbereiche von Playwright 2026 —
von der Grundkonfiguration über Page Object Model und API-Testing bis hin zu Visual Regression
und CI/CD-Integration mit GitHub Actions. Alle Codebeispiele sind direkt aus realen Projekten adaptiert.
01
Grundlagen & Konfiguration
03
Assertions & Debugging
06
CI/CD mit GitHub Actions
1. Playwright Grundlagen & Konfiguration
Playwright wird über npm installiert und bringt alle Browser-Binaries mit.
Die Konfigurationsdatei playwright.config.ts steuert Browser, Timeouts,
Base-URL und Reporting-Optionen zentral für das gesamte Projekt.
Install
Config
Installation und initiale Einrichtung
# Playwright installieren (interaktiver Setup-Wizard)
npm init playwright@latest
# Nur Paket hinzufügen (ohne Wizard)
npm install -D @playwright/test
# Browser-Binaries herunterladen
npx playwright install
# Nur bestimmte Browser (CI-Optimierung)
npx playwright install chromium firefox webkit
playwright.config.ts — Vollständige Produktionskonfiguration
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Verzeichnis mit Testdateien
testDir: './tests/e2e',
// Parallele Ausführung für schnellere Runs
fullyParallel: true,
workers: process.env.CI ? 4 : undefined,
// In CI strenger: kein .only erlaubt
forbidOnly: !!process.env.CI,
// Fehlgeschlagene Tests 2x wiederholen (CI-Flakiness-Schutz)
retries: process.env.CI ? 2 : 0,
// Globale Einstellungen für alle Tests
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry', // Trace nur bei Fehlern speichern
screenshot: 'only-on-failure', // Screenshots bei Fehlern
video: 'retain-on-failure', // Videos bei Fehlern behalten
actionTimeout: 10_000, // 10s pro Aktion
navigationTimeout: 30_000, // 30s für Seitennavigation
},
// Reporter: HTML-Report lokal, GitHub-Annotations in CI
reporter: process.env.CI
? [['github'], ['html', { open: 'never' }]]
: [['html'], ['line']],
// Browser-Matrix: 3 Engines × Desktop + Mobile
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 15'] },
},
],
// Dev-Server vor Tests starten (nur lokal)
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Erster Test — Locator-Strategien und Auto-Waiting
Playwright setzt auf semantische Locators: getByRole, getByText,
getByLabel und getByPlaceholder sind stabiler als CSS-Selektoren,
weil sie testen wie ein Benutzer interagiert — nicht wie der Code aufgebaut ist.
Jede Aktion wartet automatisch, bis das Element actionable ist (sichtbar, aktiviert, stabil).
import { test, expect } from '@playwright/test';
test.describe('Produkt-Suche', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('findet Produkt über Suchfeld', async ({ page }) => {
// Semantischer Locator — kein CSS, kein XPath
await page.getByRole('searchbox', { name: 'Produktsuche' }).fill('Laptop');
await page.getByRole('button', { name: 'Suchen' }).click();
// Auto-waiting: wartet auf Netzwerkruhe nach Click
await page.waitForLoadState('networkidle');
// Assertion — Playwright wartet automatisch bis Bedingung erfüllt
await expect(page.getByRole('listitem')).toHaveCount.greaterThan(0);
await expect(page.getByText('Laptop Pro 16"')).toBeVisible();
await expect(page).toHaveURL(/search\?q=Laptop/);
});
test('zeigt Fehlermeldung bei leerem Suchfeld', async ({ page }) => {
await page.getByRole('button', { name: 'Suchen' }).click();
const error = page.getByRole('alert');
await expect(error).toBeVisible();
await expect(error).toHaveText('Bitte Suchbegriff eingeben');
});
test('Keyboard-Navigation funktioniert', async ({ page }) => {
await page.getByRole('searchbox').fill('Notebook');
await page.keyboard.press('Enter');
// getByTestId als letzter Ausweg wenn keine semantische Rolle verfügbar
await expect(page.getByTestId('search-results')).toBeVisible();
});
});
Locator-Priorität (Best Practice 2026)
getByRole — ARIA-Rollen (button, link, heading, listitem)
getByLabel — Form-Inputs über ihr Label
getByPlaceholder — Inputs mit Placeholder-Text
getByText — Sichtbarer Text-Inhalt
getByAltText — Bilder über Alt-Text
getByTestId — data-testid-Attribut (letzter Ausweg)
- CSS-Selektoren — NUR wenn keine semantische Alternative existiert
2. Page Object Model (POM)
POM
Fixtures
Das Page Object Model kapselt alle Interaktionen mit einer Seite in einer Klasse.
Tests werden dadurch kürzer, lesbarer und wartbarer: Ändert sich ein Selektor,
muss er nur an einem Ort angepasst werden.
Playwright Fixtures erweitern das Muster um automatische Instanziierung.
BasePage — Gemeinsame Basisklasse
// tests/pages/BasePage.ts
import { type Page, type Locator } from '@playwright/test';
export abstract class BasePage {
protected readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate(path = '/'): Promise<void> {
await this.page.goto(path);
await this.page.waitForLoadState('domcontentloaded');
}
async waitForNetworkIdle(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
async getPageTitle(): Promise<string> {
return this.page.title();
}
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true });
}
}
LoginPage — Konkrete Page-Klasse
// tests/pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
// Locators als readonly properties — einmal definiert, überall verwendbar
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('E-Mail-Adresse');
this.passwordInput = page.getByLabel('Passwort');
this.submitButton = page.getByRole('button', { name: 'Anmelden' });
this.errorMessage = page.getByRole('alert');
this.forgotPasswordLink = page.getByRole('link', { name: 'Passwort vergessen?' });
}
// Reusable Action: vollständiger Login-Flow
async login(email: string, password: string): Promise<void> {
await this.navigate('/login');
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
await this.waitForNetworkIdle();
}
async expectError(message: string): Promise<void> {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
async expectRedirectToDashboard(): Promise<void> {
await expect(this.page).toHaveURL('/dashboard');
}
}
DashboardPage
// tests/pages/DashboardPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class DashboardPage extends BasePage {
readonly welcomeHeading: Locator;
readonly statsGrid: Locator;
readonly recentOrdersTable: Locator;
readonly logoutButton: Locator;
readonly notificationBadge: Locator;
constructor(page: Page) {
super(page);
this.welcomeHeading = page.getByRole('heading', { level: 1 });
this.statsGrid = page.getByTestId('stats-grid');
this.recentOrdersTable = page.getByRole('table');
this.logoutButton = page.getByRole('button', { name: 'Abmelden' });
this.notificationBadge = page.getByTestId('notification-count');
}
async getStatValue(label: string): Promise<string> {
const stat = this.page.getByTestId(`stat-${label}`);
return (await stat.textContent()) ?? '';
}
async expectLoaded(): Promise<void> {
await expect(this.welcomeHeading).toBeVisible();
await expect(this.statsGrid).toBeVisible();
}
}
Fixtures — automatische Page-Instanziierung
// tests/fixtures.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
// Typen für eigene Fixtures definieren
type Fixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: DashboardPage; // bereits eingeloggt
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.navigate('/login');
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
// Fixture mit vorausgehendem Login-Schritt
authenticatedPage: async ({ page }, use) => {
const login = new LoginPage(page);
await login.login(
process.env.TEST_EMAIL ?? 'test@example.com',
process.env.TEST_PASSWORD ?? 'TestPass123!'
);
await login.expectRedirectToDashboard();
await use(new DashboardPage(page));
},
});
export { expect };
Test mit POM und Fixtures
// tests/e2e/auth.spec.ts — sauber und lesbar dank POM
import { test, expect } from '../fixtures';
test.describe('Authentifizierung', () => {
test('erfolgreicher Login leitet auf Dashboard weiter',
async ({ loginPage, dashboardPage }) => {
await loginPage.login('user@example.com', 'SecurePass!');
await dashboardPage.expectLoaded();
}
);
test('falsches Passwort zeigt Fehlermeldung',
async ({ loginPage }) => {
await loginPage.login('user@example.com', 'wrong');
await loginPage.expectError('Ungültige Anmeldedaten');
}
);
test('Dashboard-Statistiken nach Login korrekt',
async ({ authenticatedPage }) => {
await authenticatedPage.expectLoaded();
const revenue = await authenticatedPage.getStatValue('revenue');
expect(revenue).toMatch(/€[\d,.]+/);
}
);
});
POM-Vorteile auf einen Blick
- Selektor-Änderungen nur an einer Stelle
- Tests lesen sich wie Prosa (“loginPage.login(...)”)
- Fixtures: kein Setup-Boilerplate in jedem Test
- Parallele Tests ohne Zustandsprobleme
- Einfache Wiederverwendung über mehrere Test-Suites
3. Assertions & Debugging
Assertions
Trace Viewer
Playwright-Assertions sind auto-retrying: Sie versuchen die Bedingung
bis zu einem Timeout (Standard: 5 Sekunden) zu erfüllen, bevor der Test fehlschlägt.
Das eliminiert fragile waitFor-Aufrufe und macht Tests robuster.
Häufig verwendete Web-First-Assertions
import { test, expect } from '@playwright/test';
test('Assertions Showcase', async ({ page }) => {
await page.goto('/produkte');
// Sichtbarkeit
await expect(page.getByRole('heading', { name: 'Produkte' })).toBeVisible();
await expect(page.getByTestId('loader')).toBeHidden();
// Text-Inhalt
await expect(page.getByTestId('product-count')).toHaveText('42 Produkte');
await expect(page.getByTestId('page-title')).toContainText('Produkte');
// URL und Titel
await expect(page).toHaveURL('/produkte');
await expect(page).toHaveTitle(/Produkte/);
// Formular-Elemente
await expect(page.getByLabel('Suche')).toBeEnabled();
await expect(page.getByLabel('Kategorie')).toHaveValue('alle');
// Attribute und CSS
await expect(page.getByRole('img', { name: 'Logo' })).toHaveAttribute('alt', 'SpockyMagicAI');
await expect(page.getByTestId('status-dot')).toHaveCSS('background-color', 'rgb(34, 197, 94)');
// Listen und Anzahl
const cards = page.getByTestId('product-card');
await expect(cards).toHaveCount(12); // erste Seite = 12 Einträge
});
Soft Assertions — alle Fehler sammeln
// Soft Assertions: Test läuft komplett durch, auch wenn ein expect fehlschlägt
test('Produktdetail-Seite vollständig', async ({ page }) => {
await page.goto('/produkte/laptop-pro-16');
// Alle Assertions werden geprüft, nicht beim ersten Fehler abgebrochen
await expect.soft(page.getByRole('heading', { level: 1 })).toBeVisible();
await expect.soft(page.getByTestId('price')).toContainText('€');
await expect.soft(page.getByRole('button', { name: 'In den Warenkorb' })).toBeEnabled();
await expect.soft(page.getByAltText('Produktbild')).toBeVisible();
await expect.soft(page.getByTestId('rating')).toHaveText(/[\d.]+\/5/);
// Am Ende: schlägt fehl wenn IRGENDEINE soft assertion fehlschlug
expect(test.info().errors).toHaveLength(0);
});
Debugging: UI Mode, Trace Viewer und Screenshots
# Interactive UI Mode — Tests live beobachten und debuggen
npx playwright test --ui
# Bestimmte Testdatei im headed Browser ausführen
npx playwright test auth.spec.ts --headed
# Nur ein spezifischer Test per -g (grep)
npx playwright test -g "erfolgreicher Login"
# Trace direkt mitaufzeichnen
npx playwright test --trace on
# Trace lokal öffnen nach fehlgeschlagenem Test
npx playwright show-trace test-results/auth-login/trace.zip
# Debug-Modus: Playwright Inspector öffnet sich
PWDEBUG=1 npx playwright test auth.spec.ts
// Manueller Screenshot im Test (für Fehleranalyse)
test('Warenkorb-Flow', async ({ page }) => {
await page.goto('/produkte/laptop-pro-16');
await page.getByRole('button', { name: 'In den Warenkorb' }).click();
// Screenshot zum Debugging zwischenspeichern
await page.screenshot({ path: 'debug/after-add-to-cart.png' });
// Auf Warenkorb-Feedback warten
await expect(page.getByRole('alert')).toContainText('Produkt hinzugefügt');
// Vollständige Seite screenshotten
await page.screenshot({ path: 'debug/cart-confirmation.png', fullPage: true });
});
4. API-Testing mit Playwright
API
Backend Seeding
Playwright enthält einen eingebauten API-Testing-Client (APIRequestContext),
der sich nahtlos mit den Browser-Tests kombinieren lässt. Damit können APIs direkt getestet werden,
ohne einen Browser zu starten — ideal für Backend-Seeding vor E2E-Tests oder als eigene API-Test-Suite.
Direkte API-Calls: GET, POST, PUT, DELETE
import { test, expect } from '@playwright/test';
test.describe('Produkt-API', () => {
const API_BASE = 'http://localhost:3001/api';
test('GET /products — Liste zurückgeben', async ({ request }) => {
const response = await request.get(`${API_BASE}/products`);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toHaveProperty('data');
expect(body.data).toBeInstanceOf(Array);
expect(body.data.length).toBeGreaterThan(0);
});
test('POST /products — neues Produkt anlegen', async ({ request }) => {
const newProduct = {
name: 'Test-Laptop 2026',
price: 1299.99,
category: 'laptops',
stock: 50,
};
const response = await request.post(`${API_BASE}/products`, {
headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
data: newProduct,
});
expect(response.status()).toBe(201);
const created = await response.json();
expect(created).toMatchObject({ name: 'Test-Laptop 2026', price: 1299.99 });
expect(created).toHaveProperty('id');
});
test('PUT /products/:id — Produkt aktualisieren', async ({ request }) => {
const response = await request.put(`${API_BASE}/products/42`, {
headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
data: { price: 999.99, stock: 25 },
});
expect(response.ok()).toBeTruthy();
const updated = await response.json();
expect(updated.price).toBe(999.99);
});
test('DELETE /products/:id — Produkt löschen', async ({ request }) => {
const response = await request.delete(`${API_BASE}/products/99`, {
headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
});
expect(response.status()).toBe(204);
});
});
Backend-Seeding: API vor E2E-Tests befüllen
// tests/e2e/checkout.spec.ts — Testdaten per API anlegen, dann E2E testen
import { test, expect } from '@playwright/test';
interface Product { id: number; name: string; price: number }
test.describe('Checkout-Flow', () => {
let testProduct: Product;
// Setup: Testprodukt per API anlegen
test.beforeAll(async ({ request }) => {
const res = await request.post('/api/products', {
headers: { Authorization: `Bearer ${process.env.SEED_TOKEN}` },
data: { name: 'E2E-Testprodukt', price: 49.99, stock: 100 },
});
testProduct = await res.json();
});
// Teardown: Testprodukt per API wieder löschen
test.afterAll(async ({ request }) => {
await request.delete(`/api/products/${testProduct.id}`, {
headers: { Authorization: `Bearer ${process.env.SEED_TOKEN}` },
});
});
test('Produkt kaufen — vollständiger Flow', async ({ page }) => {
await page.goto(`/produkte/${testProduct.id}`);
await page.getByRole('button', { name: 'In den Warenkorb' }).click();
await page.getByRole('link', { name: 'Zum Warenkorb' }).click();
await expect(page.getByText(testProduct.name)).toBeVisible();
await expect(page.getByTestId('cart-total')).toContainText('€49.99');
});
});
APIRequestContext — globaler Client mit Auth
// tests/fixtures.ts — APIRequestContext mit Auth-Header
import { test as base, request } from '@playwright/test';
export const test = base.extend({
apiContext: async ({}, use) => {
const ctx = await request.newContext({
baseURL: process.env.API_BASE_URL ?? 'http://localhost:3001',
extraHTTPHeaders: {
Accept: 'application/json',
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
await use(ctx);
await ctx.dispose();
},
});
5. Visual Regression Testing
Visual
Snapshots
Playwright kann Screenshot-Diffs gegen Baseline-Images vergleichen.
Pixel-genaue Vergleiche erkennen unbeabsichtigte CSS-Änderungen, Layout-Verschiebungen
und Rendering-Unterschiede zwischen Browsers — bevor sie in Produktion gelangen.
Basis-Setup: toHaveScreenshot
import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('Startseite sieht korrekt aus', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Screenshot gegen Baseline vergleichen
await expect(page).toHaveScreenshot('startseite.png', {
maxDiffPixels: 100, // Toleranz: max 100 Pixel-Unterschied
threshold: 0.2, // 20% Farbdifferenz erlaubt (Anti-Aliasing)
fullPage: true,
});
});
test('Produkt-Karte visuell korrekt', async ({ page }) => {
await page.goto('/produkte');
await page.waitForLoadState('networkidle');
// Nur einen bestimmten Bereich der Seite screenshotten
const firstCard = page.getByTestId('product-card').first();
await expect(firstCard).toHaveScreenshot('produkt-karte.png');
});
test('Mobile-Ansicht — Hamburger-Menü', async ({ page }) => {
// Viewport auf Mobile setzen
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/');
await page.getByRole('button', { name: 'Menü öffnen' }).click();
await expect(page).toHaveScreenshot('mobile-menu-open.png');
});
});
Baseline-Management und Update-Workflow
# Baselines erstmals generieren (beim ersten Run)
npx playwright test --update-snapshots
# Snapshots für einen bestimmten Browser aktualisieren
npx playwright test --update-snapshots --project=chromium
# Nur visual-regression Tests laufen lassen
npx playwright test visual-regression.spec.ts
# Diff-Report nach fehlgeschlagenem Run anzeigen
npx playwright show-report
pixelmatch-Konfiguration in playwright.config.ts
export default defineConfig({
// ... andere Optionen ...
// Globale Snapshot-Einstellungen
expect: {
// Screenshots in eigenem Verzeichnis ablegen
snapshotDir: './tests/snapshots',
toHaveScreenshot: {
// Pixelmatch-Schwellwert (0-1): 0.2 = 20% Toleranz pro Pixel
threshold: 0.2,
// Max. absolute Pixel-Unterschiede (für Animation/Fonts)
maxDiffPixels: 50,
// Animationen stoppen für stabile Vergleiche
animations: 'disabled',
// Caret (Cursor) in Inputs verstecken
caret: 'hide',
// Skalierung: css = device-unabhängig
scale: 'css',
},
},
});
Visual-Regression Best Practices
- Immer
waitForLoadState('networkidle') vor Screenshot
- Dynamische Inhalte (Datum, Zufallsdaten) mit
page.evaluate einfrieren
- Separate Snapshot-Ordner pro Browser-Projekt
- Baseline-Updates als eigenen PR behandeln und reviewen
- Nur strukturelle Änderungen = Update; unbeabsichtigte = Bug-Fix
6. CI/CD-Integration mit GitHub Actions
GitHub Actions
Sharding
Matrix
GitHub Actions ist die natürliche Heimat für Playwright-Tests.
Der offizielle Playwright-Action installiert Dependencies und Browser in einem Schritt.
Mit Browser-Matrix und Test-Sharding laufen auch große Test-Suites in wenigen Minuten.
Vollständige GitHub Actions Workflow-Datei
# .github/workflows/e2e-playwright.yml
name: Playwright E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
playwright:
name: 'Playwright (${{ matrix.project }})'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false # Alle Browsers laufen, auch wenn einer fehlschlägt
matrix:
project: [chromium, firefox, webkit]
shard: [1/3, 2/3, 3/3] # Test-Suite in 3 Shards aufteilen
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Node.js einrichten
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Dependencies installieren
run: npm ci
- name: Playwright Browser installieren
run: npx playwright install --with-deps ${{ matrix.project }}
- name: Dev-Server starten (Hintergrund)
run: npm run build && npm run start &
env:
NODE_ENV: test
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
API_TOKEN: ${{ secrets.TEST_API_TOKEN }}
- name: Warten bis Server bereit
run: npx wait-on http://localhost:3000 --timeout 60000
- name: Playwright Tests ausführen
run: |
npx playwright test \
--project=${{ matrix.project }} \
--shard=${{ matrix.shard }} \
--reporter=html
env:
CI: 'true'
BASE_URL: 'http://localhost:3000'
- name: HTML-Report als Artefakt hochladen
uses: actions/upload-artifact@v4
if: always() # Auch bei Testfehlern uploaden!
with:
name: playwright-report-${{ matrix.project }}-${{ strategy.job-index }}
path: playwright-report/
retention-days: 7
- name: Traces hochladen (nur bei Fehler)
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.project }}-${{ strategy.job-index }}
path: test-results/
retention-days: 3
Sharding ohne Matrix — für einfachere Setups
# Shard 1 von 4 laufen lassen
npx playwright test --shard=1/4
# In GitHub Actions ohne Matrix-Strategy:
# Jeder Job bekommt eine andere Shard-Nummer via ${{ matrix.shardIndex }}
strategy:
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
Retry-Konfiguration und Flakiness-Management
// playwright.config.ts — CI-Retries
export default defineConfig({
retries: process.env.CI ? 2 : 0,
// Flaky-Test-Report: zeigt welche Tests mehrere Versuche brauchten
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }], // für GitHub PR-Summary
],
use: {
// Bei Retry: Trace immer aufzeichnen
trace: 'on-first-retry',
video: 'retain-on-failure',
},
});
Browser-Matrix: Chromium vs. Firefox vs. WebKit
| Browser |
Engine |
Testabdeckung |
Typische Laufzeit |
Pflicht in CI? |
| Chromium |
Blink |
Chrome + Edge |
~2 min |
Ja (Primary) |
| Firefox |
Gecko |
Firefox alle Versionen |
~3 min |
Ja |
| WebKit |
WebKit |
Safari macOS + iOS |
~4 min |
Ja (Safari-Bugs) |
| Mobile Chrome |
Blink |
Android Chrome |
~2.5 min |
Empfohlen |
| Mobile Safari |
WebKit |
iPhone Safari |
~4 min |
Empfohlen |
Performance-Tipp: Mit Sharding (z.B. --shard=1/4) und
fullyParallel: true laufen 200+ Tests in unter 5 Minuten in CI.
Nutze test.skip mit Browser-Kondition statt Browser aus der Matrix zu entfernen:
test.skip(browserName === 'webkit', 'WebKit unterstützt keine WebRTC')
Test-Report in GitHub PR-Summary anzeigen
# .github/workflows/e2e-playwright.yml — PR-Kommentar mit Test-Ergebnissen
- name: Test-Report als PR-Kommentar
uses: dawidd6/action-send-mail@v3
# Oder: Playwright GitHub Reporter nutzt automatisch GitHub Annotations
# → rote X und Checks direkt im PR ohne extra Step
# Empfehlung: GitHub Reporter in playwright.config.ts aktivieren
# reporter: process.env.CI ? [['github'], ['html']] : [['html']]
# → Playwright schreibt ::error:: Annotations in GitHub Actions Logs
CI/CD Checkliste für Playwright
npm ci statt npm install für reproduzierbare Builds
npx playwright install --with-deps installiert Browser-Binaries + OS-Deps
- Artefakte mit
if: always() hochladen (auch bei Fehler)
fail-fast: false in Matrix damit alle Browser-Ergebnisse vorliegen
- Secrets nie in Logs ausgeben —
env:-Block in Steps nutzen
retention-days: 7 spart Storage-Kosten bei Artefakten
Claude Code für deine Playwright-Tests
Page Object Model, API-Seeding, Visual Regression und GitHub Actions CI —
Claude Code versteht den kompletten Playwright-Stack und generiert produktionsreife E2E-Tests
für React, Vue, Next.js und SvelteKit. Jetzt 14 Tage kostenlos testen.
14 Tage kostenlos testen →