Skip to content

Getting Started with Playwright

Playwright is Microsoft’s modern end-to-end testing framework that enables fast, reliable testing across all major browsers. Unlike older tools, Playwright is built for the modern web with features like auto-wait, network interception, and mobile emulation out of the box.

  • Fast & Reliable: Built-in auto-wait eliminates flaky tests
  • Cross-Browser: Test Chrome, Firefox, Safari with one codebase
  • Modern Features: Screenshots, videos, network mocking, mobile testing
  • Developer Experience: Excellent debugging tools and VS Code integration
  • Language Support: JavaScript, TypeScript, Python, .NET, Java

Before starting, ensure you have:

  • Node.js 16+ installed
  • Code editor (VS Code recommended for best experience)
  • Basic knowledge of JavaScript/TypeScript
Terminal window
# Create project directory
mkdir playwright-tutorial
cd playwright-tutorial
# Initialize package.json
npm init -y
Terminal window
# Install Playwright Test
npm install -D @playwright/test
# Download browser binaries
npx playwright install

The browser download is about 200MB and includes Chromium, Firefox, and WebKit.

Terminal window
npx playwright init

This creates:

  • playwright.config.ts - Test configuration
  • tests/ - Your test files directory
  • tests/example.spec.ts - Sample test file

After initialization, your project looks like:

playwright-tutorial/
├── node_modules/
├── tests/
│ └── example.spec.ts
├── playwright.config.ts
├── package.json
└── package-lock.json

Let’s create a meaningful test that verifies a real website:

tests/playwright-homepage.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Playwright Homepage', () => {
test('should load and display main navigation', async ({ page }) => {
// Navigate to Playwright homepage
await page.goto('https://playwright.dev');
// Verify page loaded correctly
await expect(page).toHaveTitle(/Playwright/);
// Check main navigation is visible
await expect(page.getByRole('link', { name: 'Docs' })).toBeVisible();
await expect(page.getByRole('link', { name: 'API' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Community' })).toBeVisible();
});
test('should navigate to getting started guide', async ({ page }) => {
await page.goto('https://playwright.dev');
// Click "Get started" button
await page.getByRole('link', { name: 'Get started' }).first().click();
// Verify we're on the installation page
await expect(page).toHaveURL(/.*\/intro/);
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
test('should search for specific content', async ({ page }) => {
await page.goto('https://playwright.dev');
// Open search
await page.getByRole('button', { name: 'Search' }).click();
// Search for "locators"
await page.getByPlaceholder('Search docs').fill('locators');
await page.keyboard.press('Enter');
// Verify search results appear
await expect(page.getByText('Locators')).toBeVisible();
});
});
Terminal window
# Run all tests
npx playwright test
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific test file
npx playwright test playwright-homepage
# Run single test
npx playwright test -g "should load and display main navigation"
Terminal window
# Run in debug mode
npx playwright test --debug
# Debug specific test
npx playwright test --debug -g "should navigate"

In debug mode, you can:

  • Step through test execution
  • Inspect page elements
  • View console output
  • Take screenshots manually

Here’s a practical playwright.config.ts setup:

import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Test directory
testDir: './tests',
// Run tests in parallel
fullyParallel: true,
// Fail build on CI if you accidentally left test.only in source
forbidOnly: !!process.env.CI,
// Retry failed tests
retries: process.env.CI ? 2 : 0,
// Limit workers on CI
workers: process.env.CI ? 1 : undefined,
// Test timeout
timeout: 30000,
// Expect timeout
expect: {
timeout: 5000
},
// Reporter configuration
reporter: [
['html'],
['json', { outputFile: 'test-results.json' }]
],
// Global test settings
use: {
// Base URL for page.goto('/')
baseURL: 'https://playwright.dev',
// Capture trace on retry
trace: 'on-first-retry',
// Screenshot on failure
screenshot: 'only-on-failure',
// Record video on failure
video: 'retain-on-failure',
},
// Configure browsers
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile testing
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
});

Playwright uses a hierarchy of locator strategies:

// 1. Role-based (RECOMMENDED)
await page.getByRole('button', { name: 'Submit' });
await page.getByRole('textbox', { name: 'Email' });
await page.getByRole('link', { name: 'Home' });
// 2. Test ID (for unique elements)
await page.getByTestId('user-menu');
await page.getByTestId('checkout-button');
// 3. Text content
await page.getByText('Welcome back');
await page.getByText('Total: $29.99');
// 4. Label association
await page.getByLabel('Password');
await page.getByLabel('Remember me');
// 5. CSS selectors (when necessary)
await page.locator('.navbar-toggle');
await page.locator('#main-content');

Playwright automatically waits for elements to be ready:

// These actions auto-wait for element to be:
// - Visible
// - Stable (not animating)
// - Enabled
// - Not covered by other elements
await page.getByRole('button', { name: 'Save' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.getByRole('textbox').type('Hello world');

Use Playwright expect for reliable assertions:

// Element visibility
await expect(page.getByText('Success')).toBeVisible();
await expect(page.getByRole('button', { name: 'Delete' })).toBeHidden();
// Text content
await expect(page.getByRole('heading')).toHaveText('Welcome');
await expect(page.locator('.status')).toContainText('Active');
// Page-level assertions
await expect(page).toHaveTitle('Dashboard');
await expect(page).toHaveURL(/.*\/dashboard/);
// Form values
await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
await expect(page.getByRole('checkbox')).toBeChecked();

Create reusable page objects for complex pages:

pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.getByTestId('error-message');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectErrorMessage(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
}
test-data/users.ts
export const testUsers = {
validUser: {
email: 'test@example.com',
password: 'SecurePass123'
},
invalidUser: {
email: 'invalid@example.com',
password: 'wrongpass'
}
};
// In your test
import { testUsers } from '../test-data/users';
test('login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await page.goto('/login');
await loginPage.login(testUsers.validUser.email, testUsers.validUser.password);
});
fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
export const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
});
export { expect } from '@playwright/test';

After running tests, view the HTML report:

Terminal window
npx playwright show-report

The report includes:

  • Test execution timeline
  • Screenshots of failures
  • Trace files for debugging
  • Network activity logs

Now that you have Playwright running, explore these advanced topics:

  • API Testing: Combine UI and API tests
  • Visual Testing: Screenshot comparisons
  • Mobile Testing: Device emulation
  • CI/CD Integration: GitHub Actions setup
  • Advanced Patterns: Custom fixtures and utilities
Terminal window
# Reinstall browsers
npx playwright install --force
# Install system dependencies (Linux)
npx playwright install-deps
// Add explicit waits when needed
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="content"]');
// Use soft assertions for non-critical checks
await expect.soft(page.getByText('Optional text')).toBeVisible();

You now have a solid foundation with Playwright! The next guide will cover building a complete test framework with page objects, utilities, and CI integration.