Playwright Configuration and Usage
Playwright is a powerful end-to-end testing framework that enables reliable testing for modern web apps. This document covers how Playwright is configured and used in this project for end-to-end testing.
Overview
- Version: 1.5x
- Capabilities: Cross-browser testing, mobile viewport testing, visual testing, and more
- Browsers: Chrome, Firefox, WebKit (Safari)
Configuration
The project should have a Playwright configuration file. If not created yet, you can create a playwright.config.ts file at the project root with the following configuration:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './e2e-tests',
  timeout: 30000,
  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'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});
Setting Up Playwright
If not already set up, you can initialize Playwright with:
npx playwright install
This installs required browsers and dependencies.
Directory Structure
Create an e2e-tests directory at the project root with the following structure:
e2e-tests/
├── fixtures/         # Test data and fixtures
├── helpers/          # Helper functions for tests
├── pages/            # Page object models
│   ├── HomePage.ts
│   ├── LoginPage.ts
│   └── ...
└── specs/            # Test specifications
    ├── auth.spec.ts
    ├── navigation.spec.ts
    └── ...
Writing Tests
1. Basic Test
// e2e-tests/specs/home.spec.ts
import { test, expect } from '@playwright/test';
test('homepage has title and links', async ({ page }) => {
  await page.goto('/');
  
  // Verify title
  await expect(page).toHaveTitle(/Next.js Starter/);
  
  // Check for navigation links
  await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
  await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
});
2. Using Page Object Model Pattern
Create page object models to encapsulate page interactions:
// e2e-tests/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  
  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
  }
  
  async goto() {
    await this.page.goto('/login');
  }
  
  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}
// Use in test
// e2e-tests/specs/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('user can login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('test@example.com', 'password123');
  
  // Verify successful login
  await expect(page.getByText('Welcome back')).toBeVisible();
});
3. Testing API and UI Together
// e2e-tests/specs/profile.spec.ts
import { test, expect } from '@playwright/test';
test('user can update profile', async ({ page, request }) => {
  // Login via API for speed
  const loginResponse = await request.post('/api/auth/login', {
    data: {
      email: 'test@example.com',
      password: 'password123'
    }
  });
  
  // Extract cookies from response
  const cookies = loginResponse.headers()['set-cookie'];
  if (cookies) {
    await page.context().addCookies(
      cookies.map(c => {
        const [name, value] = c.split('=');
        return { name, value, domain: 'localhost', path: '/' };
      })
    );
  }
  
  // Navigate to profile page
  await page.goto('/profile');
  
  // Update profile
  await page.getByLabel('Display Name').fill('Updated Name');
  await page.getByRole('button', { name: 'Save Changes' }).click();
  
  // Verify success message
  await expect(page.getByText('Profile updated successfully')).toBeVisible();
  
  // Verify data was actually updated
  await page.reload();
  await expect(page.getByLabel('Display Name')).toHaveValue('Updated Name');
});
Running Tests
Add the following scripts to your package.json:
"scripts": {
  "test:e2e": "playwright test",
  "test:e2e:ui": "playwright test --ui",
  "test:e2e:debug": "playwright test --debug",
  "test:e2e:report": "playwright show-report"
}
Default commands:
- 
Run all tests: Run all Playwright tests headlessly npm run test:e2e
- 
Run with UI mode: Run tests with the UI mode for debugging npm run test:e2e:ui
- 
Debug tests: Run tests in debug mode with browser window npm run test:e2e:debug
- 
Show report: Open the HTML report after tests complete npm run test:e2e:report
Visual Testing
Playwright supports visual comparison testing:
// e2e-tests/specs/visual.spec.ts
import { test, expect } from '@playwright/test';
test('homepage visual regression', async ({ page }) => {
  await page.goto('/');
  
  // Take a screenshot and compare with baseline
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixelRatio: 0.01
  });
});
To update snapshots when UI changes:
npx playwright test --update-snapshots
Authentication Helpers
Create helpers for common authentication scenarios:
// e2e-tests/helpers/auth.ts
import { Page, APIRequestContext } from '@playwright/test';
export async function loginViaApi(request: APIRequestContext, page: Page) {
  const response = await request.post('/api/auth/login', {
    data: {
      email: 'test@example.com',
      password: 'password123'
    }
  });
  
  // Store authentication state
  await page.context().storageState({ path: './e2e-tests/fixtures/auth-state.json' });
  
  return response;
}
// Use in tests
// e2e-tests/specs/protected-page.spec.ts
import { test } from '@playwright/test';
import { loginViaApi } from '../helpers/auth';
test.beforeEach(async ({ page, request }) => {
  await loginViaApi(request, page);
});
test('authenticated user can access protected page', async ({ page }) => {
  await page.goto('/dashboard');
  // Test protected content
});
Best Practices
- Use data-testid attributes: Add data-testidattributes to elements for stable selectors
- Isolate tests: Make tests independent of each other
- Clean up after tests: Reset state between tests, especially database state
- Use API for setup: Set up test data via API calls rather than UI interactions when possible
- Run in CI: Include Playwright tests in your CI workflow
- Wait for right events: Use proper waiting mechanisms instead of arbitrary delays
- Use the Page Object Model: Encapsulate page interactions in page objects
- Take screenshots for debugging: Use screenshots to debug test failures