Playwright Tests in Drupal 11: Modernes End-to-End Testing

Steven Schulz
Steven Schulz

Playwright Tests in Drupal 11: Der ultimative Guide

End-to-End Testing ist in modernen Drupal-Projekten unverzichtbar geworden. Während PHPUnit und Drupal’s FunctionalJavascript Tests ihre Berechtigung haben, bietet Playwright eine moderne, schnelle und developer-friendly Alternative für umfassende Browser-Tests.

Was ist Playwright?

Playwright ist ein Open-Source Framework von Microsoft für zuverlässige End-to-End Tests. Im Gegensatz zu Selenium bietet Playwright:

  • Auto-Waiting: Keine manuellen sleep() oder waitFor() Calls nötig
  • Multi-Browser Support: Chromium, Firefox, WebKit mit einer API
  • Blazing Fast: Headless Tests in Sekunden statt Minuten
  • Developer Tools: Test Generator, Inspector, Trace Viewer
  • Native Mobile Testing: iPhone, iPad, Android Emulation

Für Drupal 11 bedeutet das: Tests für komplexe Editorial Workflows, Layout Builder Konfigurationen oder Webform-Submissions laufen stabil und schnell.

Installation in Drupal 11 Projekten

Voraussetzungen

# Node.js 18+ erforderlich
node --version  # v18.0.0 oder höher

# Drupal 11 Installation mit Composer
composer require drupal/core:^11.0

Playwright Setup

# Im Drupal-Root oder separatem tests/ Verzeichnis
npm init playwright@latest

# Installation mit Prompts:
# - TypeScript (empfohlen)
# - tests/ als Testverzeichnis
# - GitHub Actions workflow (optional)

Dies erstellt folgende Struktur:

tests/
|-- playwright.config.ts    # Hauptkonfiguration
|-- example.spec.ts         # Beispieltest
\-- fixtures/              # Custom Fixtures
package.json
playwright.config.ts

Drupal-spezifische Konfiguration

Passe playwright.config.ts an:

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

export default defineConfig({
  testDir: './tests',

  // Drupal ist oft langsamer - längere Timeouts
  timeout: 30 * 1000,
  expect: {
    timeout: 10000
  },

  // Screenshots bei Fehlern
  use: {
    baseURL: 'http://drupal11.ddev.site',  // Deine lokale URL
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },

  // Parallele Tests
  workers: process.env.CI ? 2 : 4,

  // Browser-Matrix
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'mobile',
      use: { ...devices['iPhone 13'] },
    },
  ],

  // Dev Server (optional)
  webServer: {
    command: 'ddev start',
    url: 'http://drupal11.ddev.site',
    reuseExistingServer: !process.env.CI,
  },
});

Dein erster Drupal Test

Einfacher Login-Test

// tests/auth/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Drupal Login', () => {

  test('Admin kann sich einloggen', async ({ page }) => {
    // Zur Login-Seite navigieren
    await page.goto('/user/login');

    // Formular ausfüllen
    await page.fill('#edit-name', 'admin');
    await page.fill('#edit-pass', 'admin123');

    // Login-Button klicken
    await page.click('#edit-submit');

    // Erfolgreiche Weiterleitung prüfen
    await expect(page).toHaveURL('/admin/content');

    // Admin Toolbar sichtbar?
    await expect(page.locator('#toolbar-administration')).toBeVisible();

    // Willkommensnachricht prüfen
    await expect(page.locator('.messages--status'))
      .toContainText('You have logged in');
  });

  test('Falsches Passwort zeigt Fehlermeldung', async ({ page }) => {
    await page.goto('/user/login');

    await page.fill('#edit-name', 'admin');
    await page.fill('#edit-pass', 'wrong-password');
    await page.click('#edit-submit');

    // Fehlermeldung erscheint
    await expect(page.locator('.messages--error'))
      .toContainText('Unrecognized username or password');

    // Noch auf Login-Seite
    await expect(page).toHaveURL(/.*user\/login/);
  });

});

Test ausführen

# Alle Tests
npx playwright test

# Spezifischer Test
npx playwright test login.spec.ts

# Mit UI (Debug Mode)
npx playwright test --ui

# In einem Browser
npx playwright test --headed --project=chromium

Fortgeschrittene Drupal Test-Patterns

1. Authentication Fixtures

Vermeide wiederholte Logins mit State-Reuse:

// tests/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';

export const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Login einmal durchführen
    await page.goto('/user/login');
    await page.fill('#edit-name', 'admin');
    await page.fill('#edit-pass', 'admin123');
    await page.click('#edit-submit');

    // Session speichern
    await page.context().storageState({
      path: 'tests/.auth/admin.json'
    });

    await use(page);
  },
});

// Verwendung:
test('Create Article', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/node/add/article');
  // ... bereits eingeloggt!
});

2. Content Creation Tests

// tests/content/article.spec.ts
test('Create and Publish Article', async ({ authenticatedPage: page }) => {
  await page.goto('/node/add/article');

  // Titel eingeben
  await page.fill('#edit-title-0-value', 'Mein Playwright Test-Artikel');

  // Body mit CKEditor
  const editorFrame = page.frameLocator('.cke_wysiwyg_frame');
  await editorFrame.locator('body').fill(
    'Dies ist der Inhalt meines Test-Artikels.'
  );

  // Taxonomie-Tag hinzufügen
  await page.fill('#edit-field-tags-0-target-id', 'Testing');

  // Bild hochladen
  await page.setInputFiles(
    '#edit-field-image-0-upload',
    'tests/fixtures/test-image.jpg'
  );
  await page.waitForSelector('.image-widget', { state: 'visible' });

  // Als veröffentlicht markieren
  await page.check('#edit-status-value');

  // Speichern
  await page.click('#edit-submit');

  // Erfolgsmeldung prüfen
  await expect(page.locator('.messages--status'))
    .toContainText('Article Mein Playwright Test-Artikel has been created');

  // Artikel erscheint auf Frontpage?
  await page.goto('/');
  await expect(page.locator('.node--type-article'))
    .toContainText('Mein Playwright Test-Artikel');
});

3. Layout Builder Tests

test('Layout Builder: Add Block', async ({ authenticatedPage: page }) => {
  await page.goto('/admin/structure/types/manage/page/display');

  // Layout Builder aktivieren
  await page.check('#edit-layout-enabled');
  await page.click('#edit-submit');

  // Test-Page erstellen
  await page.goto('/node/add/page');
  await page.fill('#edit-title-0-value', 'Layout Test Page');
  await page.click('#edit-submit');

  // Layout bearbeiten
  await page.click('a:has-text("Layout")');
  await page.click('a:has-text("Add block")');

  // Block hinzufügen
  await page.click('a:has-text("Custom block")');
  await page.fill('#edit-settings-label', 'Test Block');
  await page.fill('#edit-settings-body-value', 'Block Content');
  await page.click('button:has-text("Add block")');

  // Layout speichern
  await page.click('button:has-text("Save layout")');

  // Block ist sichtbar
  await expect(page.locator('.block-inline-block'))
    .toContainText('Block Content');
});

4. Webform Submission Tests

test('Webform: Contact Form Submission', async ({ page }) => {
  await page.goto('/contact');

  // Formular ausfüllen
  await page.fill('#edit-name', 'Max Mustermann');
  await page.fill('#edit-email', 'max@example.com');
  await page.fill('#edit-subject-0-value', 'Test Anfrage');
  await page.fill('#edit-message-0-value', 'Dies ist eine Test-Nachricht.');

  // Honeypot sollte leer bleiben
  await expect(page.locator('#edit-website')).toBeHidden();

  // Absenden
  await page.click('#edit-submit');

  // Bestätigung
  await expect(page.locator('.messages--status'))
    .toContainText('Your message has been sent');

  // In DB geprüft (via API)
  const response = await page.request.get('/api/webform-submissions/contact');
  expect(response.ok()).toBeTruthy();
});

Performance Testing mit Playwright

test('Homepage Performance', async ({ page }) => {
  // Performance Metrics sammeln
  await page.goto('/', { waitUntil: 'networkidle' });

  const metrics = await page.evaluate(() => {
    const navigation = performance.getEntriesByType('navigation')[0];
    return {
      domContentLoaded: navigation.domContentLoadedEventEnd,
      loadComplete: navigation.loadEventEnd,
      firstPaint: performance.getEntriesByType('paint')[0]?.startTime,
    };
  });

  // Assertions
  expect(metrics.domContentLoaded).toBeLessThan(3000); // 3s
  expect(metrics.loadComplete).toBeLessThan(5000);     // 5s

  // Lighthouse Audit (mit Playwright-Lighthouse)
  const { playAudit } = require('playwright-lighthouse');
  await playAudit({
    page,
    thresholds: {
      performance: 80,
      accessibility: 90,
      'best-practices': 85,
      seo: 90,
    },
  });
});

Visual Regression Testing

test('Homepage Visual Regression', async ({ page }) => {
  await page.goto('/');

  // Screenshot vergleichen
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixels: 100,
  });
});

test('Responsive Design Check', async ({ page }) => {
  await page.goto('/');

  // Desktop
  await page.setViewportSize({ width: 1920, height: 1080 });
  await expect(page).toHaveScreenshot('homepage-desktop.png');

  // Tablet
  await page.setViewportSize({ width: 768, height: 1024 });
  await expect(page).toHaveScreenshot('homepage-tablet.png');

  // Mobile
  await page.setViewportSize({ width: 375, height: 667 });
  await expect(page).toHaveScreenshot('homepage-mobile.png');
});

CI/CD Integration

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Setup Drupal
        run: |
          composer install
          ./vendor/bin/drush site:install -y
          ./vendor/bin/drush config:import -y

      - name: Run Playwright Tests
        run: npx playwright test

      - name: Upload Test Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

GitLab CI

# .gitlab-ci.yml
playwright:
  image: mcr.microsoft.com/playwright:v1.40.0-focal

  script:
    - npm ci
    - npx playwright install
    - composer install
    - ./vendor/bin/drush site:install -y
    - npx playwright test

  artifacts:
    when: always
    paths:
      - playwright-report/
    expire_in: 1 week

Best Practices für Drupal + Playwright

1. Nutze Data Attributes für Selectors

// [Fragil] - ändert sich mit Theme-Updates
await page.click('.button.button--primary.form-submit');

// [Stabil] - nutze data-testid
await page.click('[data-testid="submit-article"]');

In Drupal Twig Templates:

<button
  type="submit"
  data-testid="submit-article"
  class="{{ button_classes }}">
  {{ 'Submit'|t }}
</button>

2. Page Object Pattern

// tests/pages/ArticlePage.ts
export class ArticlePage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/node/add/article');
  }

  async fillTitle(title: string) {
    await this.page.fill('#edit-title-0-value', title);
  }

  async fillBody(content: string) {
    const frame = this.page.frameLocator('.cke_wysiwyg_frame');
    await frame.locator('body').fill(content);
  }

  async submit() {
    await this.page.click('#edit-submit');
  }

  async expectSuccess() {
    await expect(this.page.locator('.messages--status'))
      .toContainText('has been created');
  }
}

// Verwendung:
test('Create Article with POM', async ({ authenticatedPage }) => {
  const article = new ArticlePage(authenticatedPage);
  await article.goto();
  await article.fillTitle('Test Artikel');
  await article.fillBody('Test Content');
  await article.submit();
  await article.expectSuccess();
});

3. Schnellere Tests mit Drush

// tests/fixtures/drupal.fixture.ts
import { test as base } from '@playwright/test';
import { execSync } from 'child_process';

export const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Login via Drush statt Browser
    const sessionCookie = execSync(
      './vendor/bin/drush user:login --uid=1 --uri=http://drupal11.ddev.site'
    ).toString().match(/session=([^&]+)/)?.[1];

    await page.context().addCookies([
      {
        name: 'SSESS[...]',
        value: sessionCookie,
        domain: 'drupal11.ddev.site',
        path: '/',
      }
    ]);

    await use(page);
  },
});

4. Test Data Management

// tests/fixtures/test-data.ts
export async function createTestArticle(page: Page) {
  const response = await page.request.post('/jsonapi/node/article', {
    data: {
      data: {
        type: 'node--article',
        attributes: {
          title: 'Test Article',
          body: { value: 'Test content', format: 'basic_html' },
          status: true,
        },
      },
    },
    headers: {
      'Content-Type': 'application/vnd.api+json',
      'X-CSRF-Token': await getCSRFToken(page),
    },
  });

  return await response.json();
}

// Cleanup nach Tests
test.afterEach(async ({ page }) => {
  await page.request.delete('/jsonapi/node/article/TEST_UUID');
});

5. Accessibility Testing

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('Homepage meets WCAG 2.1 Level AA', async ({ page }) => {
  await page.goto('/');

  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();

  expect(accessibilityScanResults.violations).toEqual([]);
});

Debugging Playwright Tests

1. UI Mode (interaktiv)

npx playwright test --ui
  • Time Travel Debugging
  • Watch Mode
  • DOM Snapshots
  • Network Inspector

2. Headed Mode (Browser sichtbar)

npx playwright test --headed --project=chromium

3. Debug Mode mit Breakpoints

test('Debug Example', async ({ page }) => {
  await page.goto('/user/login');

  // Browser stoppt hier
  await page.pause();

  await page.fill('#edit-name', 'admin');
  // ... weiter
});

4. Trace Viewer

# Test mit Trace aufnehmen
npx playwright test --trace on

# Trace öffnen
npx playwright show-trace trace.zip

Zeigt:

  • DOM Snapshots bei jedem Step
  • Console Logs
  • Network Requests
  • Screenshots
  • Timeline

5. Screenshots & Videos

test('Debug mit Screenshots', async ({ page }) => {
  await page.goto('/admin/content');

  // Manueller Screenshot
  await page.screenshot({ path: 'debug-screenshot.png' });

  // Element-spezifisch
  await page.locator('.view-content').screenshot({
    path: 'content-list.png'
  });
});

Vergleich: Playwright vs. Drupal FunctionalJavascript

FeaturePlaywrightDrupal FunctionalJavascript
SpeedSehr schnell (Headless)Langsamer (Symfony BrowserKit)
BrowserChromium, Firefox, WebKitChrome/Firefox via WebDriver
Auto-WaitingJa (Eingebaut)Nein (Manuell mit assertSession()->waitFor())
DebuggingUI Mode, Trace Viewer, Inspector--verbose, dump(), Breakpoints
CI/CDDocker Images verfügbarDrupal Site + Webdriver Setup nötig
Setupnpm init playwrightTeil von Drupal Core
Test LanguageTypeScript/JavaScriptPHP
Parallel TestsJa (Out-of-the-box)Bedingt (Mit Test Traits möglich)
Mobile TestingJa (Device Emulation)Nein (Nicht unterstützt)
Best forUser Journeys, E2E, Visual TestsDrupal-spezifische Integration

Empfehlung: Nutze beide komplementär:

  • FunctionalJavascript: Drupal Core/Module Funktionen
  • Playwright: Kritische User Workflows, Cross-Browser, Performance

Typische Drupal Use Cases

1. Content Moderation Workflow

test('Article Draft -> Review -> Published', async ({ page }) => {
  // Als Editor einloggen
  await loginAs(page, 'editor');

  // Draft erstellen
  await page.goto('/node/add/article');
  await page.fill('#edit-title-0-value', 'Review Me');
  await page.selectOption('#edit-moderation-state-0-state', 'draft');
  await page.click('#edit-submit');

  // Als Reviewer einloggen
  await loginAs(page, 'reviewer');
  await page.goto('/admin/content/moderated');
  await page.click('a:has-text("Review Me")');

  // In Review setzen
  await page.click('a:has-text("Edit")');
  await page.selectOption('#edit-moderation-state-0-state', 'in_review');
  await page.click('#edit-submit');

  // Als Publisher freigeben
  await loginAs(page, 'publisher');
  await page.goto('/admin/content/moderated');
  await page.click('a:has-text("Review Me")');
  await page.click('a:has-text("Edit")');
  await page.selectOption('#edit-moderation-state-0-state', 'published');
  await page.click('#edit-submit');

  // Artikel ist live
  await page.goto('/');
  await expect(page.locator('.node--type-article'))
    .toContainText('Review Me');
});

2. Commerce Checkout

test('Drupal Commerce: Add to Cart & Checkout', async ({ page }) => {
  await page.goto('/shop/products');

  // Produkt hinzufügen
  await page.click('[data-product-id="123"] .add-to-cart');
  await expect(page.locator('.cart-block'))
    .toContainText('1 item');

  // Checkout
  await page.goto('/cart');
  await page.click('a:has-text("Checkout")');

  // Adresse
  await page.fill('#edit-payment-information-billing-address-given-name', 'Max');
  await page.fill('#edit-payment-information-billing-address-family-name', 'Mustermann');
  await page.fill('#edit-payment-information-billing-address-address-line1', 'Teststr. 123');
  await page.fill('#edit-payment-information-billing-address-postal-code', '12345');
  await page.fill('#edit-payment-information-billing-address-locality', 'Berlin');

  // Testgateway
  await page.selectOption('#edit-payment-information-payment-method', 'test_gateway');
  await page.fill('#edit-payment-information-test-card-number', '4111111111111111');

  // Order abschließen
  await page.click('#edit-submit');

  await expect(page.locator('.checkout-complete'))
    .toContainText('Your order number is');
});

3. Media Library Tests

test('Media Library: Upload & Embed Image', async ({ authenticatedPage: page }) => {
  await page.goto('/node/add/article');

  // Media Library öffnen
  await page.click('button:has-text("Add media")');

  // In Modal: Upload
  const modal = page.locator('.ui-dialog');
  await modal.locator('input[type="file"]').setInputFiles('tests/fixtures/test.jpg');

  // Alt-Text eingeben
  await modal.fill('#edit-media-0-fields-field-media-image-0-alt', 'Test Image');

  // Media speichern
  await modal.click('button:has-text("Save")');

  // Media auswählen
  await modal.click('.media-library-item input[type="checkbox"]');
  await modal.click('button:has-text("Insert selected")');

  // Im WYSIWYG eingebettet
  const editor = page.frameLocator('.cke_wysiwyg_frame');
  await expect(editor.locator('img[alt="Test Image"]')).toBeVisible();

  // Artikel speichern
  await page.fill('#edit-title-0-value', 'Article with Media');
  await page.click('#edit-submit');

  // Bild wird angezeigt
  await expect(page.locator('.field--name-body img'))
    .toHaveAttribute('alt', 'Test Image');
});

Erweiterte Features

1. API Testing (JSON:API)

test('JSON:API: Create Node via REST', async ({ request }) => {
  // Auth Token holen
  const tokenResponse = await request.post('/user/login?_format=json', {
    data: {
      name: 'admin',
      pass: 'admin123'
    }
  });
  const token = (await tokenResponse.json()).csrf_token;

  // Node erstellen
  const response = await request.post('/jsonapi/node/article', {
    headers: {
      'Content-Type': 'application/vnd.api+json',
      'X-CSRF-Token': token,
    },
    data: {
      data: {
        type: 'node--article',
        attributes: {
          title: 'API Test Article',
          body: {
            value: 'Created via JSON:API',
            format: 'basic_html'
          }
        }
      }
    }
  });

  expect(response.ok()).toBeTruthy();

  const body = await response.json();
  const nodeUuid = body.data.id;

  // Cleanup
  await request.delete(`/jsonapi/node/article/${nodeUuid}`, {
    headers: { 'X-CSRF-Token': token }
  });
});

2. Multi-Language Tests

test('Multilingual Content', async ({ authenticatedPage: page }) => {
  // Deutsch
  await page.goto('/de/node/add/article');
  await page.fill('#edit-title-0-value', 'Deutscher Titel');
  await page.click('#edit-submit');

  // Übersetzung hinzufügen
  await page.click('a:has-text("Translate")');
  await page.click('a:has-text("English")');
  await page.fill('#edit-title-0-value', 'English Title');
  await page.click('#edit-submit');

  // Language Switcher testen
  await page.goto('/de/deutscher-titel');
  await expect(page.locator('h1')).toContainText('Deutscher Titel');

  await page.click('a[hreflang="en"]');
  await expect(page.locator('h1')).toContainText('English Title');
  await expect(page).toHaveURL(/\/en\/english-title/);
});

3. Search API Tests

test('Search API: Index & Search', async ({ page }) => {
  // Trigger Indexierung (via Drush oder UI)
  await page.goto('/admin/config/search/search-api');
  await page.click('button:has-text("Index now")');

  await expect(page.locator('.messages--status'))
    .toContainText('Successfully indexed');

  // Suche durchführen
  await page.goto('/');
  await page.fill('#edit-search-api-fulltext', 'Drupal');
  await page.click('#edit-submit');

  // Ergebnisse prüfen
  await expect(page.locator('.search-results')).toBeVisible();
  await expect(page.locator('.search-result').first())
    .toContainText('Drupal');
});

Monitoring & Reporting

1. Custom HTML Reporter

// playwright.config.ts
export default defineConfig({
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'junit.xml' }],
  ],
});

2. Slack Notifications

// tests/utils/slack-reporter.ts
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';

class SlackReporter implements Reporter {
  onEnd(result: FullResult) {
    const message = {
      text: `Playwright Tests: ${result.status}`,
      attachments: [{
        color: result.status === 'passed' ? 'good' : 'danger',
        fields: [
          { title: 'Passed', value: result.passed, short: true },
          { title: 'Failed', value: result.failed, short: true },
        ]
      }]
    };

    fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      body: JSON.stringify(message)
    });
  }
}

export default SlackReporter;

3. Grafana Dashboard Integration

test.afterEach(async ({ page }, testInfo) => {
  const metrics = await page.evaluate(() => ({
    duration: performance.now(),
    memory: performance.memory?.usedJSHeapSize,
  }));

  // An Prometheus Pushgateway senden
  await fetch('http://pushgateway:9091/metrics/job/playwright', {
    method: 'POST',
    body: `
      test_duration_ms{test="${testInfo.title}"} ${testInfo.duration}
      test_status{test="${testInfo.title}"} ${testInfo.status === 'passed' ? 1 : 0}
    `
  });
});

Troubleshooting

Problem: Tests sind instabil / “flaky”

Lösung 1: Nutze Auto-Waiting richtig

// [Flaky]
await page.click('#submit');
await expect(page.locator('.success')).toBeVisible(); // Kann zu früh sein

// [Robust]
await page.click('#submit');
await page.waitForLoadState('networkidle');
await expect(page.locator('.success')).toBeVisible();

Lösung 2: Erhöhe Timeouts für langsame Drupal-Operationen

test('Slow Drupal Operation', async ({ page }) => {
  test.setTimeout(60000); // 60s für diesen Test

  await page.goto('/admin/config/development/performance');
  await page.click('#edit-clear', { timeout: 30000 }); // 30s für Cache Clear
});

Problem: CKEditor nicht erkannt

Lösung: Frame-Handling

// CKEditor 4 (iFrame)
const editorFrame = page.frameLocator('.cke_wysiwyg_frame');
await editorFrame.locator('body').fill('Text');

// CKEditor 5 (kein iFrame)
await page.locator('.ck-editor__editable').fill('Text');

Problem: AJAX-Requests nicht abgewartet

Lösung: Network Idle oder Response Waiting

// Option 1: Network Idle
await page.goto('/admin/content', { waitUntil: 'networkidle' });

// Option 2: Spezifische Response warten
await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/jsonapi')),
  page.click('#ajax-trigger')
]);

Problem: Headless vs. Headed unterschiedlich

Lösung: Browser-Argumente anpassen

// playwright.config.ts
use: {
  launchOptions: {
    args: [
      '--disable-blink-features=AutomationControlled',
      '--disable-web-security', // Nur für lokale Tests!
    ]
  }
}

Kosten-Nutzen-Analyse

Setup-Zeit

  • Initial: 2-4 Stunden (Installation + Konfiguration)
  • Erster Test: 30 Minuten
  • 10 Tests: ~1 Tag
  • CI/CD Integration: 2-3 Stunden

Wartungsaufwand

  • Stabil: ~2-5% der ursprünglichen Entwicklungszeit
  • Selectors mit data-testid: Kaum Wartung bei Theme-Updates
  • Page Object Pattern: Änderungen zentral an einer Stelle

ROI Break-Even

  • Kleine Projekte (1-2 Entwickler): Nach ~3 Monaten
  • Mittelgroße Projekte (3-5 Entwickler): Nach ~6 Wochen
  • Enterprise (5+ Entwickler): Nach ~2 Wochen

Verhinderte Bugs (Durchschnitt)

  • Pre-Production: 12-15 Bugs/Monat verhindert
  • Production: 3-5 kritische Incidents/Jahr vermieden
  • Kosten pro Production Bug: ~2.000-5.000€
  • Einsparung: 6.000-25.000€/Jahr

Zusammenfassung

Playwright bietet Drupal-Entwicklern eine moderne, schnelle Alternative zu traditionellen Testing-Tools. Mit Auto-Waiting, Multi-Browser-Support und exzellenten Developer Tools lassen sich robuste End-to-End Tests schreiben, die:

  • Schneller laufen als Selenium/WebDriver
  • Weniger maintenance benötigen durch Auto-Waiting
  • Besser debuggbar sind mit UI Mode & Trace Viewer
  • Cross-Browser out-of-the-box funktionieren
  • CI/CD-ready sind mit Docker Images

Kombiniert mit Drupal’s Backend-Tests (PHPUnit, FunctionalJavascript) entsteht eine umfassende Testing-Strategie, die Qualität sichert und Entwicklungszeit spart.

Nächste Schritte:

  1. npm init playwright@latest in deinem Drupal-Projekt ausführen
  2. Ersten Login-Test schreiben (5 Minuten)
  3. Page Object Pattern für häufige Workflows implementieren
  4. CI/CD Pipeline mit GitHub Actions aufsetzen
  5. Visual Regression Tests für kritische Seiten hinzufügen

Ressourcen:


Benötigen Sie Unterstützung bei der Playwright-Integration in Ihr Drupal-Projekt? Mit über 20 Jahren Drupal-Erfahrung und Testing-Expertise helfe ich Ihnen gerne bei der Implementierung einer professionellen Test-Strategie. Kontakt: mail@stevenschulz.net oder 04037420859

Häufig gestellte Fragen (FAQ)

Warum Playwright statt PHPUnit für Drupal Tests?
Playwright testet die komplette User Experience im echten Browser, während PHPUnit primär Backend-Logik testet. Playwright simuliert echte Benutzerinteraktionen und erkennt JavaScript-Fehler, CSS-Probleme und visuelle Regressionen - ideal für moderne Drupal-Frontends mit React oder Vue.js Komponenten.
Kann ich Playwright parallel zu Drupal's FunctionalJavascript Tests nutzen?
Ja, beide ergänzen sich ideal. Drupal's FunctionalJavascript Tests laufen im PHP-Kontext und eignen sich für Core-Funktionen. Playwright ist besser für komplexe User Journeys, Cross-Browser Testing und Performance-Monitoring. Viele Teams nutzen beide: PHPUnit/FunctionalJavascript für Unit- und Integrationstests, Playwright für kritische End-to-End Workflows.
Wie schnell sind Playwright Tests im Vergleich?
Playwright ist deutlich schneller als Selenium: Tests laufen headless 2-3x schneller. Mit Parallelisierung können 100 Tests in unter 5 Minuten durchlaufen. Ein typischer Drupal Login-Test dauert mit Playwright ca. 2-3 Sekunden, mit Drush Session-Setup sogar unter 1 Sekunde.
Welche Browser werden unterstützt?
Playwright unterstützt Chromium, Firefox und WebKit (Safari) out-of-the-box. Tests laufen auf allen drei Engines ohne Code-Änderungen. Mobile Viewports für iPhone, iPad, Android sind ebenfalls integriert - ideal für responsive Drupal Themes.

Das könnte Sie auch interessieren

← Zurück zum Blog