End-to-end testing is crucial for ensuring your web applications work correctly from the user's perspective. Playwright, developed by Microsoft, has quickly become one of the most popular tools for browser automation and testing. This tutorial will get you up and running with Playwright in no time.
What is Playwright?
Playwright is a Node.js library that provides a high-level API to control Chromium, Firefox, and WebKit browsers. It's designed for end-to-end testing, but it's also great for web scraping, automation, and performance testing.
Key Features
- Cross-browser support: Test on Chromium, Firefox, and WebKit
- Headless and headed modes: Run tests with or without a visible browser
- Mobile testing: Emulate mobile devices and touch interactions
- Network interception: Mock API responses and test offline scenarios
- Parallel execution: Run tests in parallel for faster feedback
- Auto-wait: Automatically waits for elements to be ready
- Screenshots and videos: Capture visual evidence of test runs
Prerequisites
Before we start, make sure you have:
- Node.js 16 or higher installed
- Basic understanding of JavaScript/TypeScript
- A text editor (VS Code recommended)
- A web application to test (we'll use a demo app)
Installation and Setup
Step 1: Create a New Project
mkdir playwright-tutorial
cd playwright-tutorial
npm init -y
Step 2: Install Playwright
# Install Playwright with browsers
npm init playwright@latest
# Or install manually
npm install --save-dev @playwright/test
npx playwright install
The npm init playwright@latest command will:
- Install Playwright and its dependencies
- Download browser binaries
- Create example test files
- Set up configuration files
Step 3: Project Structure
After installation, your project should look like this:
playwright-tutorial/
├── tests/
│ └── example.spec.ts
├── playwright.config.ts
├── package.json
└── node_modules/
Step 4: Configuration
The playwright.config.ts file contains your test configuration:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run start',
port: 3000,
},
});
Writing Your First Test
Let's create a simple test that navigates to a website and verifies its content.
Step 1: Create a Basic Test
Create tests/first-test.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage has correct title', async ({ page }) => {
// Navigate to the page
await page.goto('https://playwright.dev/');
// Verify the title
await expect(page).toHaveTitle(/Playwright/);
// Check for a specific element
await expect(page.locator('h1')).toContainText('Playwright');
});
Step 2: Run Your Test
# Run all tests
npx playwright test
# Run a specific test file
npx playwright test first-test.spec.ts
# Run tests in headed mode (visible browser)
npx playwright test --headed
# Run tests in a specific browser
npx playwright test --project=firefox
Core Playwright Concepts
Page Object
The page object represents a web page in the browser:
test('page interactions', async ({ page }) => {
await page.goto('https://example.com');
// Get page title
const title = await page.title();
// Take a screenshot
await page.screenshot({ path: 'example.png' });
// Get page URL
const url = page.url();
});
Locators
Locators are Playwright's way of finding elements on the page:
test('working with locators', async ({ page }) => {
await page.goto('https://example.com');
// By text content
const heading = page.locator('text=Welcome');
// By CSS selector
const button = page.locator('button.submit');
// By data attribute
const form = page.locator('[data-testid="login-form"]');
// By role
const link = page.getByRole('link', { name: 'About' });
// By placeholder
const input = page.getByPlaceholder('Enter your email');
});
Actions
Playwright provides methods to interact with elements:
test('user interactions', async ({ page }) => {
await page.goto('https://example.com/login');
// Fill input fields
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
// Click buttons
await page.getByRole('button', { name: 'Login' }).click();
// Select from dropdown
await page.selectOption('select#country', 'us');
// Check/uncheck checkboxes
await page.getByLabel('Remember me').check();
// Upload files
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
});
Assertions
Use assertions to verify expected behavior:
test('assertions example', async ({ page }) => {
await page.goto('https://example.com');
// Page assertions
await expect(page).toHaveTitle('Example Domain');
await expect(page).toHaveURL(/example.com/);
// Element assertions
const heading = page.locator('h1');
await expect(heading).toBeVisible();
await expect(heading).toContainText('Example');
await expect(heading).toHaveClass('main-heading');
// Form assertions
const input = page.getByLabel('Email');
await expect(input).toBeEmpty();
await expect(input).toBeEnabled();
// Count assertions
const items = page.locator('.item');
await expect(items).toHaveCount(5);
});
Practical Example: Testing a Todo App
Let's create a comprehensive test for a todo application:
import { test, expect } from '@playwright/test';
test.describe('Todo App', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://todomvc.com/examples/react/');
});
test('should add a new todo', async ({ page }) => {
// Add a new todo
const newTodo = page.getByPlaceholder('What needs to be done?');
await newTodo.fill('Buy groceries');
await newTodo.press('Enter');
// Verify the todo appears in the list
await expect(page.locator('.todo-list li')).toHaveCount(1);
await expect(page.locator('.todo-list li')).toContainText('Buy groceries');
});
test('should mark todo as completed', async ({ page }) => {
// Add a todo
await page.getByPlaceholder('What needs to be done?').fill('Complete tutorial');
await page.getByPlaceholder('What needs to be done?').press('Enter');
// Mark as completed
await page.locator('.todo-list li .toggle').click();
// Verify it's marked as completed
await expect(page.locator('.todo-list li')).toHaveClass(/completed/);
});
test('should filter todos', async ({ page }) => {
// Add multiple todos
const newTodo = page.getByPlaceholder('What needs to be done?');
await newTodo.fill('Active todo');
await newTodo.press('Enter');
await newTodo.fill('Completed todo');
await newTodo.press('Enter');
// Complete one todo
await page.locator('.todo-list li').first().locator('.toggle').click();
// Filter by active
await page.getByRole('link', { name: 'Active' }).click();
await expect(page.locator('.todo-list li')).toHaveCount(1);
await expect(page.locator('.todo-list li')).toContainText('Completed todo');
// Filter by completed
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.locator('.todo-list li')).toHaveCount(1);
await expect(page.locator('.todo-list li')).toContainText('Active todo');
});
});
Advanced Features
Network Interception
Mock API responses for more reliable tests:
test('should handle API errors', async ({ page }) => {
// Mock a failed API response
await page.route('**/api/todos', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' })
});
});
await page.goto('/todos');
// Verify error handling
await expect(page.locator('.error-message')).toBeVisible();
});
Mobile Testing
Test your app on mobile devices:
import { test, expect, devices } from '@playwright/test';
test.use({
...devices['iPhone 13'],
});
test('mobile navigation', async ({ page }) => {
await page.goto('/');
// Open mobile menu
await page.getByRole('button', { name: 'Menu' }).click();
// Verify mobile-specific behavior
await expect(page.locator('.mobile-menu')).toBeVisible();
});
Visual Testing
Compare screenshots to detect visual regressions:
test('visual regression test', async ({ page }) => {
await page.goto('/dashboard');
// Take and compare screenshot
await expect(page).toHaveScreenshot('dashboard.png');
// Screenshot a specific element
await expect(page.locator('.chart')).toHaveScreenshot('chart.png');
});
Best Practices
1. Use Page Object Model
Organize your tests using the Page Object pattern:
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
async getErrorMessage() {
return this.page.locator('.error-message').textContent();
}
}
// test file
test('login with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('invalid@email.com', 'wrongpassword');
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});
2. Use Data-Testid Attributes
Add test-specific attributes to make tests more reliable:
<!-- In your HTML -->
<button data-testid="submit-button">Submit</button>
// In your test
await page.getByTestId('submit-button').click();
3. Wait Strategies
Use appropriate waiting strategies:
// Wait for element to be visible
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
// Wait for network request
await page.waitForResponse('**/api/data');
// Wait for function to return true
await page.waitForFunction(() => window.dataLoaded === true);
4. Test Data Management
Use fixtures for test data:
// tests/fixtures.ts
export const testUsers = {
admin: { email: 'admin@test.com', password: 'admin123' },
user: { email: 'user@test.com', password: 'user123' }
};
// In your test
import { testUsers } from './fixtures';
test('admin can access admin panel', async ({ page }) => {
await loginPage.login(testUsers.admin.email, testUsers.admin.password);
await expect(page.locator('.admin-panel')).toBeVisible();
});
Debugging Tests
Running Tests in Debug Mode
# Debug a specific test
npx playwright test --debug first-test.spec.ts
# Use Playwright Inspector
npx playwright test --ui
Adding Debug Information
test('debug example', async ({ page }) => {
await page.goto('/');
// Pause test execution
await page.pause();
// Add console logs
console.log('Current URL:', page.url());
// Take screenshot for debugging
await page.screenshot({ path: 'debug.png' });
});
Running Tests in CI/CD
GitHub Actions Example
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- 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@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Next Steps
Now that you've learned the basics of Playwright, here are some areas to explore further:
- Advanced Selectors: Learn about CSS selectors, XPath, and text selectors
- API Testing: Use Playwright's API testing capabilities
- Performance Testing: Measure and test web performance
- Accessibility Testing: Integrate accessibility testing into your suite
- Custom Fixtures: Create reusable test fixtures and helpers
- Parallel Testing: Scale your tests with parallel execution
- Integration: Integrate with CI/CD pipelines and reporting tools
Troubleshooting Common Issues
Tests are Flaky
- Use
page.waitForLoadState()to ensure page is fully loaded - Add explicit waits for dynamic content
- Use
page.waitForSelector()instead ofpage.locator().click()
Elements Not Found
- Check if element is in an iframe:
page.frameLocator('iframe').locator('button') - Verify element timing: use
{ timeout: 10000 }for slow elements - Use more specific selectors or data-testid attributes
Slow Tests
- Run tests in parallel:
npx playwright test --workers=4 - Use
page.route()to mock slow API calls - Optimize selectors and reduce unnecessary waits
Conclusion
Playwright is a powerful tool for end-to-end testing that makes browser automation reliable and efficient. With its cross-browser support, auto-waiting capabilities, and rich API, it's an excellent choice for testing modern web applications.
The key to successful E2E testing with Playwright is to:
- Start with simple tests and gradually add complexity
- Use reliable selectors and waiting strategies
- Organize tests with Page Object Model
- Mock external dependencies when possible
- Run tests in CI/CD for continuous feedback
With these fundamentals, you're ready to build a robust test suite that will catch bugs before they reach your users.
Ready to implement Playwright in your project? Get in touch to discuss testing strategies specific to your application and team needs.