Zum Inhalt springen

E2E Testing with Playwright for PHP Websites

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

Playwright is often associated with SPAs — React, Vue, Angular. But it works just as well for classic server-side rendered PHP websites. In my projects, I use Playwright for Symfony sites with dynamic JavaScript components, multilingual navigation, and form validation.

Why Playwright Instead of Cypress or Selenium?

  • Multi-browser: Chromium, Firefox, WebKit in a single framework
  • Mobile viewports: iPhone, Pixel — viewport and user-agent emulation
  • Auto-waiting: Automatically waits for DOM elements and network requests
  • TypeScript-native: First-class TypeScript support without workarounds

Configuration for 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'] }
        },
    ],
});

Basic Page Tests

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();
});

Testing Multilingual Navigation

For bilingual sites (DE/EN), I systematically test the language switch:

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

    // Click on English flag
    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);
    }
});

Testing Mobile Navigation

On mobile viewports, the navigation is collapsed. The most common mistake: testing links that are hidden behind the hamburger menu.

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

    if (isMobile) {
        // Open hamburger menu
        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/);
});

Testing Async Components

When JavaScript components load data via API, Playwright needs to wait for the loading to complete:

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

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

    // Wait for debounce (800ms) + API response
    await page.waitForSelector('.blog-search-suggestion-item', {
        timeout: 5000
    });

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

Testing Forms with CAPTCHA

ALTCHA CAPTCHAs cannot be solved in tests. The solution: In the test environment, CAPTCHA validation is skipped.

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

The Playwright test can then run through the form completely:

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 to success page
    await expect(page).toHaveURL(/erfolg|success/);
});

CI Integration

In the CI pipeline, Playwright runs headless. Important: Preserve screenshots and traces on failures:

- 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

Conclusion

Playwright is just as capable for PHP websites as it is for SPAs. The auto-wait mechanism, multi-browser support, and TypeScript integration make it the ideal tool for E2E tests — regardless of the backend stack.

Thomas Wunner

Thomas Wunner

Certified IT specialist for application development with an instructor qualification and over 14 years of experience building scalable web applications with Symfony and Shopware. When not coding, Thomas volunteers as a lifeguard with the Wasserwacht, performs as a DJ, and explores the countryside on his motorbike.

Kommentare

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