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
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:
| Feature | Playwright | Cypress | Selenium |
|---|---|---|---|
| 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()orwaitFor()needed – Playwright waits automatically getByLabel()andgetByRole()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):
1. getByRole (Recommended)
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:
- API Testing with Playwright: Same tool for frontend and backend
- Visual Regression: Screenshot comparisons for UI consistency
- Component Tests: Test individual UI components in isolation
- 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
Test Manager