TL;DR: Playwright is the most modern E2E testing framework and puts an end to flaky tests, complicated setups, and endless waiting. Auto-waiting, Trace Viewer, and native multi-browser support make it the best tool for reliable end-to-end tests. Here's why you should switch right now.

🤔 Why E2E Testing Matters — And Why Teams Keep Skipping It

Be honest: how many times have you said "We'll test that manually"? Exactly. We've all been there. Most teams write unit tests religiously. But E2E tests? They end up at the bottom of the backlog forever.

The problem: without E2E tests, you never really know if your app actually works. The login flow? The checkout? That form? All black boxes that only blow up in production.

The classic excuses are always the same:

  • Too slow
  • Too flaky
  • Too hard to set up
  • Too expensive in the CI/CD pipeline

And you know what? With Selenium and older tools, those were legitimate concerns. But Playwright changes the game entirely.

⚔️ Playwright vs. Cypress vs. Selenium — The Showdown

Before we dive in, let's compare the three big players. Choosing the right tool matters.

FeaturePlaywrightCypressSelenium
Multi-Browser✅ Chromium, Firefox, WebKit⚠️ Chromium-based + Firefox (experimental)✅ All browsers
LanguagesTypeScript, JS, Python, Java, C#JavaScript/TypeScript onlyMany languages
Auto-Waiting✅ Built-in✅ Built-in❌ Manual
Parallel Tests✅ Native⚠️ Dashboard only (paid)✅ With Grid
API Testing✅ Built-in✅ cy.request❌ Not supported
Trace Viewer✅ Amazing⚠️ Screenshots/Videos❌ No
iFrames✅ Easy⚠️ Clunky✅ Possible
Tabs / Windows✅ Multi-page native❌ Not supported✅ Possible
Speed🚀 Very fast🚀 Fast🐢 Slower
Community📈 Growing fast📊 Large📊 Huge but aging

Spoiler: Playwright wins in almost every category. And it's actively developed by Microsoft — meaning long-term support and constant improvements.

Playwright
Official Playwright documentation

🚀 Setup: Ready in 30 Seconds

Forget complicated configurations. Playwright makes getting started ridiculously easy:

npm init playwright@latest

That's it. Really. The installer asks you a few things:

  • TypeScript or JavaScript? (Pick TypeScript. Always.)
  • Where should tests go?
  • Add a GitHub Actions workflow?
  • Install browsers?
Why You Should Only Use TypeScript
TypeScript is the better choice for modern web development

After installation, you get a clean project structure:

├── tests/
│   └── example.spec.ts
├── playwright.config.ts
└── package.json

The playwright.config.ts is your command center. This is where you set browsers, timeouts, base URL, and more.

✍️ Your First Test: It's That Easy

Now for the fun part. Let's write your first Playwright test:

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

test('Homepage has correct title', async ({ page }) => {
  await page.goto('https://example.com');

  await expect(page).toHaveTitle(/Example Domain/);
});

test('Navigation works', async ({ page }) => {
  await page.goto('https://example.com');

  const link = page.getByRole('link', { name: 'More information...' });
  await link.click();

  await expect(page).toHaveURL(/iana\.org/);
});

Beautiful, right? No driver.findElement(By.xpath(...)) madness. No Thread.sleep(5000). Just clean, readable code.

Run your test:

npx playwright test

Or with UI mode for visual debugging:

npx playwright test --ui

🎯 Locator Strategies: Find Elements Like a Pro

The Locator API is the heart of Playwright. Forget fragile CSS selectors and XPath expressions. Playwright offers semantic locators that make your tests bulletproof:

// ✅ Best practice: Role-based locators
page.getByRole('button', { name: 'Sign In' });
page.getByRole('heading', { name: 'Dashboard' });
page.getByRole('textbox', { name: 'Email' });

// ✅ Text-based locators
page.getByText('Welcome back');
page.getByLabel('Password');
page.getByPlaceholder('Search...');

// ✅ Test IDs for complex cases
page.getByTestId('submit-button');
page.getByTestId('user-avatar');

// ❌ Avoid: Fragile selectors
page.locator('.btn-primary.mt-4.submit');
page.locator('#app > div:nth-child(3) > button');

The order of preference: getByRole > getByText / getByLabel > getByTestId > CSS/XPath. The more semantic, the more stable.

⏳ Auto-Waiting: Why Playwright Tests Don't Flake

This is the absolute game changer. Playwright waits for elements automatically. No sleep(). No waitForElement(). Nothing.

When you write page.click('button'), here's what happens under the hood:

  1. Wait until the element is in the DOM
  2. Wait until it's visible
  3. Wait until it's stable (no animation)
  4. Wait until it's clickable (not covered by another element)
  5. Wait until it's enabled (not disabled)
  6. Click

This eliminates the most common cause of flaky tests: race conditions between your test and your application. Your test waits exactly as long as needed — no more, no less.

Assertions auto-wait too:

// Waits up to 5 seconds (configurable) for the text to appear
await expect(page.getByText('Successfully saved')).toBeVisible();

// Waits for the URL change
await expect(page).toHaveURL('/dashboard');

🏗️ Page Object Model: Clean Test Architecture

As your test suite grows, you need structure. The Page Object Model (POM) is the proven approach — and with Playwright, it's a breeze:

// pages/login.page.ts
import { type Page, type Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign In' });
    this.errorMessage = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

And in the test:

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

test.describe('Login', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('Successful login', async ({ page }) => {
    await loginPage.login('[email protected]', 'password123');
    await expect(page).toHaveURL('/dashboard');
  });

  test('Shows error for wrong password', async () => {
    await loginPage.login('[email protected]', 'wrongpassword');
    await expect(loginPage.errorMessage).toHaveText('Invalid credentials');
  });
});

Clean, maintainable, reusable. If the login form changes, you only update the page class.

🌐 API Testing: UI and Backend in One Go

Playwright doesn't just drive browsers — it can also send HTTP requests directly. Perfect for setup steps or API validation:

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

test('API: Create user and verify in UI', async ({ page, request }) => {
  // Create user via API
  const response = await request.post('/api/users', {
    data: {
      name: 'Test User',
      email: '[email protected]',
      role: 'admin'
    }
  });
  expect(response.ok()).toBeTruthy();

  const user = await response.json();

  // Verify in UI
  await page.goto('/admin/users');
  await expect(page.getByText('Test User')).toBeVisible();

  // Clean up via API
  await request.delete(\`/api/users/\${user.id}\`);
});

The best part: you can use API calls to set up your app's state instead of clicking through the UI. This makes tests faster and more stable.

NestJS: Server Framework on Steroids
NestJS for your backend — perfect pairing with Playwright

🔍 Trace Viewer & Debugging: No More Guessing

A test fails. Now what? With Selenium, you get a cryptic stack trace. With Playwright, you get the Trace Viewer.

# Enable traces (in playwright.config.ts)
# use: { trace: 'on-first-retry' }

# Run tests with traces
npx playwright test --trace on

# View the trace
npx playwright show-trace trace.zip

The Trace Viewer shows you:

  • Every single step of your test
  • Screenshots before and after each action
  • The complete DOM at any point in time
  • Network requests and responses
  • Console logs

It's like a time machine for your tests. You see exactly what happened and why it failed. No more guessing.

There's also the Inspector for interactive debugging:

# Start test in debug mode
npx playwright test --debug

# Or set a breakpoint in code
await page.pause();

⚙️ CI/CD Integration: GitHub Actions Example

Integrating Playwright into your CI/CD pipeline is trivial. Here's a complete GitHub Actions setup:

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - 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: Run Playwright tests
        run: npx playwright test

      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

The beauty of it: when tests fail, you have the complete report as an artifact. Download, open, debug. Done.

microsoft/playwright
Playwright on GitHub — Stars, Issues, and Releases

📸 Visual Regression Testing: Pixel-Perfect Tests

Playwright can compare screenshots and detect visual regressions. Built-in, no extra tools needed:

test('Dashboard looks correct', async ({ page }) => {
  await page.goto('/dashboard');

  // Compare full page
  await expect(page).toHaveScreenshot('dashboard.png');

  // Compare single element
  const chart = page.getByTestId('revenue-chart');
  await expect(chart).toHaveScreenshot('revenue-chart.png', {
    maxDiffPixelRatio: 0.01  // 1% tolerance
  });
});

On the first run, Playwright creates reference screenshots. On every subsequent run, it compares pixel by pixel. Changes are saved as diff images.

# Update screenshots after intentional changes
npx playwright test --update-snapshots

🔥 Practical Example: Testing a Login Flow and Form Submission

Let's put it all together. A realistic example with login and form submission:

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

test.describe('Contact Form', () => {

  test.beforeEach(async ({ page }) => {
    // Login via API for speed
    const response = await page.request.post('/api/auth/login', {
      data: { email: '[email protected]', password: 'admin123' }
    });
    const { token } = await response.json();

    // Set token in browser
    await page.goto('/');
    await page.evaluate((t) => {
      localStorage.setItem('auth_token', t);
    }, token);
  });

  test('Submit form successfully', async ({ page }) => {
    await page.goto('/contact');

    // Fill out the form
    await page.getByLabel('Name').fill('John Doe');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Subject').selectOption('support');
    await page.getByLabel('Message').fill('This is a test message.');

    // Accept checkbox
    await page.getByLabel('Accept privacy policy').check();

    // Submit
    await page.getByRole('button', { name: 'Submit' }).click();

    // Verify success
    await expect(page.getByText('Message sent successfully')).toBeVisible();
    await expect(page).toHaveURL('/contact/success');
  });

  test('Validation shows error messages', async ({ page }) => {
    await page.goto('/contact');

    // Submit empty form
    await page.getByRole('button', { name: 'Submit' }).click();

    // Check error messages
    await expect(page.getByText('Name is required')).toBeVisible();
    await expect(page.getByText('Email is required')).toBeVisible();
  });

  test('File upload works', async ({ page }) => {
    await page.goto('/contact');

    // Upload file
    const fileInput = page.getByLabel('Attachment');
    await fileInput.setInputFiles('test-data/document.pdf');

    // Check upload confirmation
    await expect(page.getByText('document.pdf')).toBeVisible();
  });
});

This example shows the power of Playwright: API-based login, form interaction, assertions with auto-waiting, and file upload — all in one clean, readable test.

💡 Conclusion

Playwright isn't just another testing tool. It's the answer to all the frustration we've had with E2E tests. Auto-waiting eliminates flaky tests. The Trace Viewer makes debugging a breeze. Multi-browser support, API testing, visual regression — all built in.

If you're still using Selenium or skipping E2E tests altogether: give Playwright a chance. Setup takes 30 seconds. Your first test is written in 5 minutes. And you'll wonder why you didn't do this sooner.

Happy Testing! 🎭

Artikel teilen:Share article: