Quality Booster

Playwright Tutorial: From First Test to CI/CD Integration

The comprehensive Playwright tutorial for beginners. Learn step-by-step how to create stable browser tests, leverage auto-waiting, and integrate into your pipeline – with practical code examples.

Andi

Andi

Test Manager

Playwright Tutorial: From First Test to CI/CD Integration

Want to automate testing your web application but tired of flaky tests and complicated setup? I’ll show you why Playwright is currently the best tool for E2E testing – and how to have your first stable test running in under an hour.

Why Playwright (and not Selenium or Cypress)?

After 15 years with Selenium and two years with Cypress, I switched to Playwright. Here’s the honest comparison:

FeaturePlaywrightCypressSelenium
Auto-Waiting✅ Built-in⚠️ Partial❌ Manual
Multi-Browser✅ Chrome, Firefox, Safari, Edge⚠️ Electron-based✅ Yes, but cumbersome
Parallelization✅ Built-in⚠️ Only with Dashboard⚠️ Grid needed
Mobile Emulation✅ Built-in❌ Not possible⚠️ Complex
Trace/Debugger✅ Excellent✅ Good❌ Poor
API Testing✅ Built-in⚠️ Cumbersome❌ Not built-in

My take: Playwright combines the reliability of Selenium with the developer experience of Cypress – and improves on both.

Installation and Setup (5 Minutes)

Prerequisites

  • Node.js 16+ (check with node --version)
  • A terminal of your choice

Step 1: Install Playwright

# Create new project (optional)
mkdir my-playwright-tests
cd my-playwright-tests

# Initialize Playwright
npm init playwright@latest

The installer will ask you about:

  • TypeScript or JavaScript: I recommend TypeScript for better autocomplete support
  • Tests directory: tests (standard, accept)
  • CI/CD Workflow: GitHub Actions (optional, can add later)

Step 2: Install Browsers

npx playwright install

This downloads Chromium, Firefox, and WebKit – about 100 MB per browser.

Step 3: Test Run

npx playwright test

If everything works, you’ll see Playwright tests running in headless mode.

Your First Test: Login Function

Let’s write a realistic test – a login flow that appears in every web app.

The Application

Assuming you have a login page with:

  • Email input field
  • Password input field
  • Submit button
  • Success message after login

The Test Code

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

test('successful login', async ({ page }) => {
  // Arrange: Open page
  await page.goto('https://my-app.com/login');
  
  // Act: Enter login data
  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('secret123');
  await page.getByRole('button', { name: 'Sign In' }).click();
  
  // Assert: Check success
  await expect(page.getByText('Welcome back')).toBeVisible();
});

What’s different from Selenium?

  • No sleep() or waitFor() needed – Playwright waits automatically
  • getByLabel() and getByRole() instead of CSS selectors – more robust against UI changes
  • The test runs deterministically, even on slow connections

The Most Important Locator Strategies

Playwright recommends this order for selectors (from robust to fragile):

await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('heading', { name: 'Dashboard' });
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');

Why: If the design changes, the test survives as long as the meaning is preserved.

2. getByLabel

await page.getByLabel('First Name').fill('John');
await page.getByLabel('I agree to the Terms').check();

Why: Links to the <label> element – very stable.

3. getByTestId (When nothing else works)

// HTML: <button data-testid="submit-order">Order</button>
await page.getByTestId('submit-order').click();

Why: Developers have full control, but requires modifying HTML.

4. CSS Selectors (Avoid)

// Fragile – breaks on design changes
await page.locator('.btn-primary').click();
await page.locator('#email-input').fill('test@example.com');

Waiting Without Waiting: Auto-Waiting

Playwright’s killer feature is automatic waiting. Compare:

❌ Old Way (Selenium/Cypress)

// Manual waiting – inefficient
await page.waitForTimeout(2000); // Hard wait
await page.waitForSelector('.loaded'); // Explicit wait
await page.click('.button');

✅ Playwright Way

// Automatic waiting – robust
await page.getByRole('button', { name: 'Order' }).click();
// Playwright waits until button is visible AND clickable

What Playwright checks automatically:

  • Element is in DOM
  • Element is visible
  • Element is not obscured (e.g., by overlay)
  • Element is enabled (not disabled)

This saves you hundreds of lines of boilerplate code.

Test Data and Environments

Fixtures: Reusable Setups

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

export const test = base.extend({
  // Logged-in user for every test
  loggedInPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill(process.env.TEST_USER!);
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
    await page.getByRole('button', { name: 'Sign In' }).click();
    await use(page);
  },
});

Usage:

import { test, expect } from './fixtures';

test('shopping cart with logged-in user', async ({ loggedInPage }) => {
  // Page is already logged in
  await loggedInPage.goto('/shop');
  await loggedInPage.getByText('Add to cart').click();
  // ...
});

Environment Variables

Create a .env file:

BASE_URL=https://staging.my-app.com
TEST_USER=test@example.com
TEST_PASSWORD=secret123

And use in playwright.config.ts:

import { defineConfig } from '@playwright/test';
import dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL,
  },
});

Debugging: When Tests Fail

Playwright’s debugging tools are first-class.

Trace Viewer

# Record with trace
npx playwright test --trace on

# View interactively
npx playwright show-report

The trace shows you:

  • Screenshots before/after each step
  • DOM changes
  • Network requests
  • Console logs

Run Tests in Visible Browser

# Chromium with UI
npx playwright test --headed

# Specific browser
npx playwright test --project=firefox --headed

VS Code Extension

Install the official Playwright extension:

  • Set breakpoints in tests
  • Step through
  • Use element inspector

CI/CD Integration

Playwright is optimized for CI/CD.

GitHub Actions

name: Playwright Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

Important: The --with-deps flag installs system dependencies for browsers.

Parallelization

Playwright runs out-of-the-box in parallel:

// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 4 : undefined, // 4 parallel workers in CI
  retries: process.env.CI ? 2 : 0, // 2 retries in CI for flakiness
});

Best Practices from the Field

1. Keep Tests Isolated

Every test should be independent – no order dependencies.

// ❌ Bad: Test B depends on Test A
test('Test A: Create user', async () => { /* ... */ });
test('Test B: Delete user', async () => { /* uses user from A */ });

// ✅ Good: Each test prepares its own setup
test('User workflow', async () => {
  const user = await createUser(); // Create in test
  await deleteUser(user.id); // And clean up
});

2. Use API Calls for Setup

Use Playwright’s API testing for faster setup:

test('complete purchase', async ({ page, request }) => {
  // Faster: API instead of UI for setup
  await request.post('/api/cart', { data: { productId: '123', quantity: 2 }});
  
  // Only test the relevant part through UI
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Buy' }).click();
});

3. Mask Sensitive Data

// In reports, password is replaced with ***
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);

4. Avoid Flakiness

// ❌ Time-based
await page.waitForTimeout(1000);

// ✅ State-based
await expect(page.getByText('Saved')).toBeVisible();

Next Steps

You now have a solid foundation. Here are the next topics:

  1. API Testing with Playwright: Same tool for frontend and backend
  2. Visual Regression: Screenshot comparisons for UI consistency
  3. Component Tests: Test individual UI components in isolation
  4. Mobile Testing: Test responsive design on different viewports

Summary

What you learned today:

✅ Installed Playwright in 5 minutes
✅ Wrote your first stable login test
✅ Learned robust locator strategies
✅ Understood why auto-waiting saves time
✅ Used fixtures for reusable setups
✅ Set up debugging tools for fast troubleshooting
✅ Configured CI/CD integration

Your next step: Write a test for your own application. Start small – a single happy-path test is better than none.

Have questions or something not working? Write me – I’m happy to help.

Andi

Andi

Test Manager