Master modern web automation with Playwright - the fastest and most reliable testing framework
Playwright is like having a super-powered robot that can control any browser. It's faster, more reliable, and easier to use than older tools like Selenium.
✓ Auto-Wait
Automatically waits for elements to be ready
✓ Multi-Browser
Test on Chrome, Firefox, Safari, Edge
✓ Fast Execution
Parallel testing out of the box
✓ Built-in Tools
Screenshots, videos, trace viewer
Locators are like addresses that help you find elements on a webpage. Think of it like finding a house - you can use the street address, the house color, or the name on the mailbox.
// 1. Role-based (BEST - accessible)
await page.getByRole('button', { name: 'Sign in' });
await page.getByRole('textbox', { name: 'Email' });
// 2. Text content
await page.getByText('Welcome back');
await page.getByLabel('Password');
// 3. Test ID (reliable for testing)
await page.getByTestId('login-button');
// 4. CSS Selector (when needed)
await page.locator('.submit-btn');
await page.locator('#username');
// 5. XPath (last resort)
await page.locator('//button[text()="Submit"]');
Use role-based locators for accessibility
Add data-testid attributes for stable tests
Avoid complex XPath expressions
Don't rely on CSS classes that might change
POM is like creating a blueprint for each page. Instead of writing locators everywhere, you create a class that represents the page with all its elements and actions.
// Repeated code in every test
test('login test 1', async ({ page }) => {
await page.fill('#username', 'user1');
await page.fill('#password', 'pass1');
await page.click('button[type="submit"]');
});
test('login test 2', async ({ page }) => {
await page.fill('#username', 'user2');
await page.fill('#password', 'pass2');
await page.click('button[type="submit"]');
});
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
// Locators
get usernameInput() {
return this.page.locator('#username');
}
get passwordInput() {
return this.page.locator('#password');
}
get submitButton() {
return this.page.getByRole('button', { name: 'Sign in' });
}
// Actions
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// Using in tests
test('login test', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'password123');
});
Web pages load at different speeds. Playwright automatically waits, but sometimes you need more control.
// Wait for element to be visible
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
// Wait for navigation
await Promise.all([
page.waitForNavigation(),
page.click('#submit-button')
]);
// Wait for API response
await page.waitForResponse(response =>
response.url().includes('/api/users') && response.status() =>= 200
);
// Custom timeout
await page.locator('.slow-element').click({ timeout: 10000 });
// Wait for element count
await expect(page.locator('.product-card')).toHaveCount(10);
Browser contexts are like incognito windows - isolated environments for testing. This allows parallel testing without interference.
// playwright.config.ts
export default defineConfig({
// Run tests in parallel
workers: 4,
// Test on multiple browsers
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
]
});
// Create isolated context
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Custom User Agent'
});
// Take screenshot
await page.screenshot({ path: 'screenshot.png' });
// Full page screenshot
await page.screenshot({ path: 'full-page.png', fullPage: true });
// Screenshot specific element
await page.locator('.header').screenshot({ path: 'header.png' });
// Enable video recording in config
use: {
video: 'on', // or 'retain-on-failure'
screenshot: 'only-on-failure'
}
Control network requests to test different scenarios without changing the backend.
// Mock API response
await page.route('**/api/users', route => route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Another User' }
])
}));
// Block images for faster tests
await page.route('**/*.{ png,jpg,jpeg }', route => route.abort());
// Simulate slow network
await page.route('**/api/**', route => {
setTimeout(() => route.continue(), 3000);
});
Build a complete test suite for an e-commerce website with POM pattern.
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { ProductPage } from '../pages/ProductPage';
import { CartPage } from '../pages/CartPage';
import { CheckoutPage } from '../pages/CheckoutPage';
test.describe('E-Commerce Checkout Flow', () => {
test('complete purchase flow', async ({ page }) => {
// 1. Login
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('test@email.com', 'password123');
// 2. Add products to cart
const productPage = new ProductPage(page);
await productPage.addToCart('Product 1');
await productPage.addToCart('Product 2');
// 3. Verify cart
const cartPage = new CartPage(page);
await cartPage.goto();
await expect(cartPage.cartItems).toHaveCount(2);
// 4. Checkout
const checkoutPage = new CheckoutPage(page);
await cartPage.proceedToCheckout();
await checkoutPage.fillShippingInfo({
address: '123 Test St',
city: 'Test City',
zip: '12345'
});
await checkoutPage.completeOrder();
// 5. Verify success
await expect(page).toHaveURL(/order-confirmation/);
await expect(page.getByText('Order Confirmed')).toBeVisible();
});
});
You've mastered Playwright web automation:
Next: Advanced web testing with Cypress in Module 3!