Chapter II - Writing Tests
(avr. time for this chapter: 2 days)
This chapter covers advanced Playwright patterns including fixtures, page object model, API testing, and test organization.
Test Fixtures
Fixtures provide isolated, reusable test setup and teardown.
Built-in Fixtures
import { test, expect } from '@playwright/test';
test('uses built-in fixtures', async ({ page, context, browser, request }) => {
// page - isolated page for each test
// context - isolated browser context
// browser - shared browser instance
// request - API request context
});
Custom Fixtures
Create tests/fixtures.ts:
import { test as base, expect } from '@playwright/test';
// Define fixture types
type MyFixtures = {
authenticatedPage: Page;
testUser: { email: string; password: string };
todoPage: TodoPage;
};
// Extend base test with custom fixtures
export const test = base.extend<MyFixtures>({
// Simple fixture
testUser: async ({}, use) => {
await use({
email: 'test@example.com',
password: 'password123'
});
},
// Fixture with setup and teardown
authenticatedPage: async ({ page }, use) => {
// Setup: Login
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('/dashboard');
// Use the authenticated page in test
await use(page);
// Teardown: Logout
await page.getByRole('button', { name: 'Logout' }).click();
},
// Page object fixture
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
},
});
export { expect };
Using Custom Fixtures
import { test, expect } from './fixtures';
test('uses authenticated page', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/profile');
await expect(authenticatedPage.getByRole('heading')).toHaveText('Profile');
});
test('uses test user data', async ({ page, testUser }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(testUser.email);
await page.getByLabel('Password').fill(testUser.password);
});
Page Object Model
Organize tests with page objects for maintainability.
Creating Page Objects
Create tests/pages/LoginPage.ts:
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
readonly successMessage: 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');
this.successMessage = page.getByTestId('success-message');
}
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();
}
async expectError(message?: string) {
await expect(this.errorMessage).toBeVisible();
if (message) {
await expect(this.errorMessage).toContainText(message);
}
}
async expectSuccess() {
await expect(this.successMessage).toBeVisible();
}
}
Create tests/pages/DashboardPage.ts:
import { Page, Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly userMenu: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome-message');
this.userMenu = page.getByTestId('user-menu');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
}
async goto() {
await this.page.goto('/dashboard');
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
async expectWelcomeMessage(name: string) {
await expect(this.welcomeMessage).toContainText(`Welcome, ${name}`);
}
}
Using Page Objects
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
test.describe('Authentication', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
});
test('successful login redirects to dashboard', async () => {
await loginPage.goto();
await loginPage.login('test@example.com', 'password123');
await dashboardPage.expectWelcomeMessage('Test User');
});
test('invalid credentials show error', async () => {
await loginPage.goto();
await loginPage.login('wrong@example.com', 'wrongpassword');
await loginPage.expectError('Invalid credentials');
});
test('logout returns to login page', async ({ page }) => {
await loginPage.goto();
await loginPage.login('test@example.com', 'password123');
await dashboardPage.logout();
await expect(page).toHaveURL('/login');
});
});
API Testing
Playwright provides powerful API testing capabilities.
Basic API Requests
import { test, expect } from '@playwright/test';
test.describe('API Tests', () => {
test('GET request', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const users = await response.json();
expect(users).toBeInstanceOf(Array);
expect(users.length).toBeGreaterThan(0);
});
test('POST request', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com'
}
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.id).toBeDefined();
expect(user.name).toBe('John Doe');
});
test('PUT request', async ({ request }) => {
const response = await request.put('/api/users/1', {
data: {
name: 'Jane Doe'
}
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe('Jane Doe');
});
test('DELETE request', async ({ request }) => {
const response = await request.delete('/api/users/1');
expect(response.status()).toBe(200);
});
});
API Authentication
test.describe('Authenticated API', () => {
let authToken: string;
test.beforeAll(async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: {
email: 'test@example.com',
password: 'password123'
}
});
const data = await response.json();
authToken = data.token;
});
test('access protected endpoint', async ({ request }) => {
const response = await request.get('/api/profile', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.ok()).toBeTruthy();
const profile = await response.json();
expect(profile.email).toBe('test@example.com');
});
});
Combining API and UI Tests
test('create user via API and verify in UI', async ({ page, request }) => {
// Create user via API
const response = await request.post('/api/users', {
data: {
name: 'New User',
email: 'newuser@example.com'
}
});
const user = await response.json();
// Verify in UI
await page.goto('/users');
await expect(page.getByText('New User')).toBeVisible();
await expect(page.getByText('newuser@example.com')).toBeVisible();
});
test('login via API and access dashboard', async ({ page, request }) => {
// Login via API
const response = await request.post('/api/auth/login', {
data: {
email: 'test@example.com',
password: 'password123'
}
});
const { token } = await response.json();
// Set token in browser storage
await page.goto('/');
await page.evaluate((token) => {
localStorage.setItem('authToken', token);
}, token);
// Access protected page
await page.goto('/dashboard');
await expect(page.getByRole('heading')).toHaveText('Dashboard');
});
Network Interception
Mocking API Responses
test('mock API response', async ({ page }) => {
// Mock the API response
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Mock User 1' },
{ id: 2, name: 'Mock User 2' }
])
});
});
await page.goto('/users');
await expect(page.getByText('Mock User 1')).toBeVisible();
await expect(page.getByText('Mock User 2')).toBeVisible();
});
Intercepting and Modifying Requests
test('modify request headers', async ({ page }) => {
await page.route('/api/**', async (route) => {
const headers = {
...route.request().headers(),
'X-Custom-Header': 'test-value'
};
await route.continue({ headers });
});
await page.goto('/');
});
test('delay response', async ({ page }) => {
await page.route('/api/users', async (route) => {
await new Promise(resolve => setTimeout(resolve, 2000));
await route.continue();
});
await page.goto('/users');
await expect(page.getByTestId('loading')).toBeVisible();
});
Simulating Errors
test('handle API error', async ({ page }) => {
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
});
});
await page.goto('/users');
await expect(page.getByTestId('error-message')).toBeVisible();
});
test('handle network failure', async ({ page }) => {
await page.route('/api/users', async (route) => {
await route.abort('failed');
});
await page.goto('/users');
await expect(page.getByText('Network error')).toBeVisible();
});
Test Organization
Test Hooks
import { test, expect } from '@playwright/test';
test.describe('User Management', () => {
// Runs once before all tests in this describe block
test.beforeAll(async ({ request }) => {
// Seed database
await request.post('/api/test/seed');
});
// Runs once after all tests in this describe block
test.afterAll(async ({ request }) => {
// Clean up database
await request.post('/api/test/cleanup');
});
// Runs before each test
test.beforeEach(async ({ page }) => {
await page.goto('/users');
});
// Runs after each test
test.afterEach(async ({ page }) => {
// Take screenshot on failure is automatic
});
test('displays user list', async ({ page }) => {
await expect(page.getByRole('table')).toBeVisible();
});
});
Test Annotations
// Skip test
test.skip('feature not implemented', async ({ page }) => {
// This test will be skipped
});
// Skip conditionally
test('skip on webkit', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'Feature not supported on WebKit');
});
// Mark as failing (expected to fail)
test.fail('known bug', async ({ page }) => {
// This test is expected to fail
});
// Focus on specific test (only runs this test)
test.only('debug this test', async ({ page }) => {
// Only this test runs
});
// Slow test (triples timeout)
test('slow operation', async ({ page }) => {
test.slow();
// Test with extended timeout
});
// Add annotations
test('annotated test', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/org/repo/issues/123'
});
});
Parameterized Tests
const users = [
{ email: 'admin@example.com', role: 'admin' },
{ email: 'user@example.com', role: 'user' },
{ email: 'guest@example.com', role: 'guest' },
];
for (const user of users) {
test(`login as ${user.role}`, async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByTestId('role')).toHaveText(user.role);
});
}
Exercise: E-Commerce Test Suite
Create a comprehensive test suite for an e-commerce application.
Step 1: Create Page Objects
// tests/pages/ProductListPage.ts
export class ProductListPage {
// Implement page object
}
// tests/pages/CartPage.ts
export class CartPage {
// Implement page object
}
// tests/pages/CheckoutPage.ts
export class CheckoutPage {
// Implement page object
}
Step 2: Create Fixtures
// tests/fixtures.ts
export const test = base.extend({
// Add fixtures for authenticated user, cart with items, etc.
});
Step 3: Write Test Suites
// tests/e-commerce/products.spec.ts
test.describe('Product Listing', () => {
test('displays all products');
test('filters by category');
test('sorts by price');
test('searches for products');
});
// tests/e-commerce/cart.spec.ts
test.describe('Shopping Cart', () => {
test('adds product to cart');
test('updates quantity');
test('removes product');
test('calculates total correctly');
});
// tests/e-commerce/checkout.spec.ts
test.describe('Checkout', () => {
test('completes purchase');
test('validates required fields');
test('applies discount code');
});
Self-Assessment
After completing this chapter, you should be able to:
- [ ] Create and use custom fixtures
- [ ] Implement the Page Object Model
- [ ] Write API tests with Playwright
- [ ] Mock and intercept network requests
- [ ] Organize tests with hooks and annotations
- [ ] Create parameterized tests
Next Steps
Continue to Chapter III - Advanced Features to learn about visual testing, parallel execution, and CI/CD integration.