Getting Started with Playwright: Modern End-to-End Testing

Learn how to set up Playwright for robust end-to-end testing with cross-browser support, powerful selectors, and advanced testing patterns.

Harry Tran
6 min read ยท 1098 words

Playwright has revolutionized end-to-end testing with its powerful API, cross-browser support, and developer-friendly features. In this comprehensive guide, we'll explore how to get started with Playwright and leverage its capabilities for robust test automation.

#What Makes Playwright Special?

Playwright stands out from other testing frameworks with several key advantages:

  • Cross-browser testing - Chromium, Firefox, and WebKit support out of the box
  • Fast execution - Parallel test execution and intelligent waiting
  • Powerful selectors - CSS, XPath, text content, and accessibility-based selectors
  • Auto-wait - Automatically waits for elements to be ready
  • Mobile testing - Device emulation and mobile browser testing
  • Network interception - Mock APIs and test offline scenarios

#Installation and Setup

Let's start by installing Playwright in your project:

bash
# Install Playwright
npm init playwright@latest
# Or add to existing project
npm install --save-dev @playwright/test

This command will:

  • Install the Playwright library
  • Add example tests
  • Configure playwright.config.js
  • Install browser binaries

#Basic Configuration

Your playwright.config.js should look like this:

javascript
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'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
})

#Writing Your First Test

Let's create a simple login test:

javascript
// tests/login.spec.js
import { test, expect } from '@playwright/test'
test('successful login flow', async ({ page }) => {
await page.goto('/login')
// Fill login form
await page.fill('[data-testid="email"]', 'user@example.com')
await page.fill('[data-testid="password"]', 'password123')
// Click login button
await page.click('button[type="submit"]')
// Verify successful login
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
await expect(page).toHaveURL('/dashboard')
})

#Powerful Selector Strategies

Playwright offers multiple ways to locate elements:

javascript
// Most reliable for testing
await page.click('[data-testid="submit-button"]')
await page.locator('[data-testid="user-profile"]').click()

#2. Text Content

javascript
// Click button with specific text
await page.click('text=Sign In')
await page.getByText('Welcome back!').click()

#3. Accessibility Roles

javascript
// Great for accessibility testing
await page.getByRole('button', { name: 'Submit' }).click()
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com')

#4. CSS Selectors

javascript
// Traditional CSS selectors
await page.click('.submit-btn')
await page.locator('#user-dropdown').click()

#Advanced Testing Patterns

#Page Object Model

Organize your tests with the Page Object Model:

javascript
// pages/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page
this.emailInput = page.locator('[data-testid="email"]')
this.passwordInput = page.locator('[data-testid="password"]')
this.submitButton = page.locator('button[type="submit"]')
}
async login(email, password) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
async goto() {
await this.page.goto('/login')
}
}
// tests/login-pom.spec.js
import { test } from '@playwright/test'
import { LoginPage } from '../pages/LoginPage'
test('login with page object model', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
})

#API Testing Integration

Combine UI and API testing:

javascript
test('user creation flow', async ({ page, request }) => {
// Create user via API
const response = await request.post('/api/users', {
data: {
email: 'newuser@example.com',
name: 'John Doe',
},
})
const user = await response.json()
// Verify user appears in UI
await page.goto('/admin/users')
await expect(page.getByText(user.email)).toBeVisible()
})

#Network Mocking

Mock API responses for consistent testing:

javascript
test('handles API errors gracefully', async ({ page }) => {
// Mock API failure
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal server error' }),
})
})
await page.goto('/users')
// Verify error message is shown
await expect(page.getByText('Failed to load users')).toBeVisible()
})

#Mobile and Responsive Testing

Test your app on different devices:

javascript
import { test, devices } from '@playwright/test'
test.describe('Mobile tests', () => {
test.use({ ...devices['iPhone 13'] })
test('mobile navigation works', async ({ page }) => {
await page.goto('/')
// Open mobile menu
await page.click('[data-testid="mobile-menu-button"]')
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible()
})
})

#Visual Testing

Catch visual regressions with screenshots:

javascript
test('homepage visual test', async ({ page }) => {
await page.goto('/')
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png')
// Element screenshot
await expect(page.locator('.hero-section')).toHaveScreenshot('hero.png')
})

#Debugging Tests

Playwright provides excellent debugging tools:

#1. Headed Mode

bash
npx playwright test --headed

#2. Debug Mode

bash
npx playwright test --debug

#3. Playwright Inspector

javascript
test('debug test', async ({ page }) => {
await page.goto('/')
await page.pause() // Opens Playwright Inspector
})

#4. Traces

javascript
// playwright.config.js
use: {
trace: 'on-first-retry', // or 'on', 'retain-on-failure'
}

View traces with:

bash
npx playwright show-trace trace.zip

#Test Organization and Best Practices

#1. Use Test Hooks

javascript
test.describe('User management', () => {
test.beforeEach(async ({ page }) => {
// Setup before each test
await page.goto('/admin')
await page.click('[data-testid="login"]')
})
test.afterEach(async ({ page }) => {
// Cleanup after each test
await page.click('[data-testid="logout"]')
})
})

#2. Parameterized Tests

javascript
const testCases = [
{ browser: 'chrome', device: 'desktop' },
{ browser: 'firefox', device: 'desktop' },
{ browser: 'safari', device: 'mobile' },
]
testCases.forEach(({ browser, device }) => {
test(`search works on ${browser} ${device}`, async ({ page }) => {
// Test implementation
})
})

#3. Custom Fixtures

javascript
// fixtures/auth.js
import { test as base } from '@playwright/test'
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Auto-login for tests that need authentication
await page.goto('/login')
await page.fill('[data-testid="email"]', 'admin@example.com')
await page.fill('[data-testid="password"]', 'password')
await page.click('button[type="submit"]')
await page.waitForURL('/dashboard')
await use(page)
},
})

#CI/CD Integration

Configure Playwright for continuous integration:

yaml
# .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

#Conclusion

Playwright provides a powerful, modern approach to end-to-end testing. Its cross-browser support, powerful selectors, and excellent developer experience make it an ideal choice for testing modern web applications.

Key benefits:

  • Reliable: Auto-wait and retry mechanisms reduce flaky tests
  • Fast: Parallel execution and efficient browser handling
  • Comprehensive: UI, API, and visual testing in one framework
  • Developer-friendly: Great debugging tools and clear error messages

Start with simple tests and gradually adopt advanced patterns like Page Object Model and custom fixtures as your test suite grows. With proper setup and best practices, Playwright will help you build confidence in your application's quality and user experience.