Zum Inhalt springen

E2E-Testing mit Playwright für PHP-Webseiten

Veröffentlicht am 10. Feb. 2026 | ca. 4 Min. Lesezeit |

Playwright wird oft mit SPAs assoziiert — React, Vue, Angular. Aber es eignet sich genauso hervorragend für klassische serverseitig gerenderte PHP-Webseiten. In meinen Projekten nutze ich Playwright für Symfony-Seiten mit dynamischen JavaScript-Komponenten, mehrsprachiger Navigation und Formularvalidierung.

Warum Playwright statt Cypress oder Selenium?

  • Multi-Browser: Chromium, Firefox, WebKit in einem Framework
  • Mobile Viewports: iPhone, Pixel — Viewport- und User-Agent-Emulation
  • Auto-Waiting: Wartet automatisch auf DOM-Elemente und Netzwerk-Requests
  • TypeScript-native: Erstklassiger TypeScript-Support ohne Workarounds

Konfiguration für Symfony

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    testDir: './e2e',
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 2 : 0,
    workers: process.env.CI ? 1 : undefined,
    reporter: 'html',
    use: {
        baseURL: process.env.BASE_URL
            || 'https://mein-projekt.ddev.site',
        ignoreHTTPSErrors: true,
        trace: 'on-first-retry',
    },
    projects: [
        {
            name: 'chromium',
            use: { ...devices['Desktop Chrome'] }
        },
        {
            name: 'mobile',
            use: { ...devices['iPhone 13'] }
        },
    ],
});

Grundlegende Seitentests

import { test, expect } from '@playwright/test';

test('homepage loads and shows hero section', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveTitle(/Wunner Software/);
    await expect(page.locator('.hero h1')).toBeVisible();
});

test('contact form renders all fields', async ({ page }) => {
    await page.goto('/kontakt');
    await expect(page.locator('input[name="contact[name]"]')).toBeVisible();
    await expect(page.locator('input[name="contact[mail]"]')).toBeVisible();
    await expect(page.locator('textarea[name="contact[message]"]')).toBeVisible();
});

Mehrsprachige Navigation testen

Für zweisprachige Seiten (DE/EN) teste ich den Sprachwechsel systematisch:

test('language switch DE to EN works', async ({ page }) => {
    await page.goto('/');
    await expect(page.locator('html')).toHaveAttribute('lang', 'de');

    // Auf englische Flagge klicken
    await page.locator('.lang-switch a[title="English"]').click();

    await expect(page.locator('html')).toHaveAttribute('lang', 'en');
    await expect(page).toHaveURL(/\/en/);
});

test('all pages return 200', async ({ page }) => {
    const routes = [
        '/', '/en',
        '/kontakt', '/en/contact',
        '/blog', '/en/blog',
        '/impressum', '/en/imprint',
        '/datenschutz', '/en/privacy',
    ];

    for (const route of routes) {
        const response = await page.goto(route);
        expect(response?.status()).toBe(200);
    }
});

Mobile Navigation testen

Auf mobilen Viewports ist die Navigation eingeklappt. Häufigster Fehler: Man testet Links, die hinter dem Hamburger-Menü versteckt sind.

test('mobile: navigation links work', async ({ page, isMobile }) => {
    await page.goto('/');

    if (isMobile) {
        // Hamburger-Menü öffnen
        await page.locator('.navbar-toggler').click();
        await page.waitForSelector('.navbar-collapse.show');
    }

    const blogLink = page.locator('.nav-link', { hasText: 'Blog' });
    await expect(blogLink).toBeVisible();
    await blogLink.click();
    await expect(page).toHaveURL(/blog/);
});

Asynchrone Komponenten testen

Wenn JavaScript-Komponenten Daten per API laden, muss Playwright auf das Laden warten:

test('blog search shows suggestions', async ({ page }) => {
    await page.goto('/blog');

    const searchInput = page.locator('#blog-search-input');
    await searchInput.fill('Symfony');

    // Auf Debounce (800ms) + API-Response warten
    await page.waitForSelector('.blog-search-suggestion-item', {
        timeout: 5000
    });

    const suggestions = page.locator('.blog-search-suggestion-item');
    await expect(suggestions.first()).toBeVisible();
});

Formulare mit CAPTCHA testen

ALTCHA-CAPTCHAs können in Tests nicht gelöst werden. Die Lösung: In der Testumgebung wird die CAPTCHA-Validierung übersprungen.

# services_test.yaml
services:
    App\Validator\AltchaChallengeValidator:
        arguments:
            $enabled: false

Der Playwright-Test kann dann das Formular komplett durchlaufen:

test('contact form submission works', async ({ page }) => {
    await page.goto('/kontakt');

    await page.fill('input[name="contact[name]"]', 'Test User');
    await page.fill('input[name="contact[mail]"]', 'test@example.com');
    await page.fill('textarea[name="contact[message]"]', 'Test message');
    await page.click('button[type="submit"]');

    // Redirect zur Erfolgsseite
    await expect(page).toHaveURL(/erfolg|success/);
});

CI-Integration

In der CI-Pipeline läuft Playwright headless. Wichtig: Screenshots und Traces bei Fehlern aufbewahren:

- name: Run Playwright tests
  run: npx playwright test
  env:
    BASE_URL: https://localhost

- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: playwright-report
    path: playwright-report/
    retention-days: 7

Fazit

Playwright ist für PHP-Webseiten genauso leistungsfähig wie für SPAs. Der Auto-Wait-Mechanismus, die Multi-Browser-Unterstützung und die TypeScript-Integration machen es zum idealen Tool für E2E-Tests — unabhängig vom Backend-Stack.

Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.