Master Playwright from core concepts to production. Learn end-to-end testing, cross-browser automation, and visual regression testing. Build a complete e-commerce web application with React and comprehensive Playwright test suites covering user flows, API mocking, and CI/CD integration.

Manual testing is slow, error-prone, and doesn't scale. As applications grow more complex, testing every user interaction manually becomes impossible. End-to-end testing automates this by simulating real user behavior across browsers and devices.
Playwright is a modern browser automation framework that makes E2E testing accessible, reliable, and fast. Used by companies like Microsoft, Google, and thousands of development teams, Playwright enables you to test web applications across Chrome, Firefox, Safari, and Edge simultaneously.
In this article, we'll explore Playwright's architecture, understand E2E testing fundamentals, and build a production-ready e-commerce web application with React that we'll test comprehensively with realistic user scenarios, API mocking, and visual regression testing.
Traditional testing approaches had significant limitations:
Manual Testing: Time-consuming, error-prone, and doesn't scale.
Selenium Limitations: Flaky tests, slow execution, complex setup.
Single Browser: Hard to test across multiple browsers simultaneously.
No Visual Testing: Can't detect UI regressions automatically.
Poor Developer Experience: Complex APIs, difficult debugging.
Slow Feedback: Tests take too long to run.
Maintenance Burden: Tests break easily with UI changes.
Playwright was built by Microsoft to solve these problems:
Fast & Reliable: Optimized for speed and stability.
Multi-Browser: Test Chrome, Firefox, Safari, Edge simultaneously.
Developer-Friendly: Simple, intuitive API.
Visual Testing: Built-in screenshot and visual regression testing.
Network Control: Mock APIs and network requests.
Debugging: Excellent debugging tools and inspector.
CI/CD Ready: Integrates seamlessly with pipelines.
Open Source: Free and community-driven.
Browser: Chromium, Firefox, or WebKit instance.
Context: Isolated browser session with separate cookies and storage.
Page: Single tab or window within a context.
Locator: Way to find elements on the page.
Action: User interaction like click, type, or navigate.
Assertion: Validation that something is true.
Fixture: Reusable test setup and teardown.
Trace: Recording of test execution for debugging.
Test Script → Playwright Engine → Browser Protocol → Browser Instance → Web ApplicationPlaywright API (JavaScript/Python/Java/C#)
↓
Playwright Server (Node.js)
↓
Browser Protocol (WebSocket)
↓
Browser Instance (Chrome/Firefox/Safari/Edge)
↓
Web ApplicationPlaywright uses browser protocol for direct communication, making it faster and more reliable than Selenium.
Automate browser interactions programmatically.
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// Navigate to URL
await page.goto('https://example.com');
// Interact with elements
await page.click('button#submit');
await page.fill('input[name="email"]', 'user@example.com');
await page.type('input[name="password"]', 'password123');
// Wait for navigation
await page.waitForNavigation();
// Take screenshot
await page.screenshot({ path: 'screenshot.png' });
await browser.close();Use Cases:
Find and interact with elements reliably.
import { test, expect } from '@playwright/test';
test('using locators', async ({ page }) => {
// CSS selector
await page.locator('button.submit').click();
// XPath
await page.locator('//button[text()="Submit"]').click();
// Text content
await page.locator('text=Submit').click();
// Role-based (recommended)
await page.locator('role=button[name="Submit"]').click();
// Chaining locators
await page.locator('form').locator('input[name="email"]').fill('user@example.com');
// Get by label
await page.getByLabel('Email').fill('user@example.com');
// Get by placeholder
await page.getByPlaceholder('Enter email').fill('user@example.com');
// Get by role
await page.getByRole('button', { name: 'Submit' }).click();
});Use Cases:
Verify application state and behavior.
import { test, expect } from '@playwright/test';
test('assertions', async ({ page }) => {
await page.goto('https://example.com');
// URL assertions
expect(page).toHaveURL('https://example.com');
// Text assertions
expect(page.locator('h1')).toContainText('Welcome');
// Visibility assertions
expect(page.locator('button')).toBeVisible();
expect(page.locator('.hidden')).toBeHidden();
// Enabled/Disabled
expect(page.locator('input')).toBeEnabled();
expect(page.locator('button')).toBeDisabled();
// Count assertions
expect(page.locator('li')).toHaveCount(5);
// Value assertions
expect(page.locator('input')).toHaveValue('expected value');
// Attribute assertions
expect(page.locator('a')).toHaveAttribute('href', '/page');
// Class assertions
expect(page.locator('div')).toHaveClass('active');
// CSS assertions
expect(page.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
});Use Cases:
Handle asynchronous operations reliably.
import { test, expect } from '@playwright/test';
test('waiting strategies', async ({ page }) => {
// Wait for navigation
await Promise.all([
page.waitForNavigation(),
page.click('a[href="/next-page"]'),
]);
// Wait for element
await page.waitForSelector('button.loaded');
// Wait for function
await page.waitForFunction(() => {
return document.querySelectorAll('li').length > 5;
});
// Wait for specific condition
await expect(page.locator('.loading')).toBeHidden();
// Wait with timeout
await page.waitForSelector('button', { timeout: 5000 });
// Auto-waiting (built-in)
// Playwright automatically waits for elements to be actionable
await page.click('button'); // Waits for button to be visible and enabled
});Use Cases:
Control and mock network requests.
import { test, expect } from '@playwright/test';
test('network mocking', async ({ page }) => {
// Mock API response
await page.route('**/api/products', (route) => {
route.abort('blockedbyclient');
});
// Mock with custom response
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]),
});
});
// Intercept and modify request
await page.route('**/api/checkout', (route) => {
const request = route.request();
route.continue({
postData: JSON.stringify({
...JSON.parse(request.postData()),
testMode: true,
}),
});
});
// Log all requests
page.on('request', (request) => {
console.log(request.method(), request.url());
});
// Log all responses
page.on('response', (response) => {
console.log(response.status(), response.url());
});
await page.goto('https://example.com');
});Use Cases:
Test across multiple browsers simultaneously.
import { test, expect, chromium, firefox, webkit } from '@playwright/test';
test('cross-browser', async () => {
for (const browserType of [chromium, firefox, webkit]) {
const browser = await browserType.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
expect(page).toHaveURL('https://example.com');
await browser.close();
}
});
// Or use test matrix
test.describe.parallel('cross-browser suite', () => {
test('chromium', async ({ browser }) => {
const page = await browser.newPage();
await page.goto('https://example.com');
});
test('firefox', async ({ browser }) => {
const page = await browser.newPage();
await page.goto('https://example.com');
});
test('webkit', async ({ browser }) => {
const page = await browser.newPage();
await page.goto('https://example.com');
});
});Use Cases:
Detect UI changes automatically.
import { test, expect } from '@playwright/test';
test('visual regression', async ({ page }) => {
await page.goto('https://example.com');
// Full page screenshot
expect(await page.screenshot()).toMatchSnapshot('homepage.png');
// Element screenshot
expect(await page.locator('header').screenshot()).toMatchSnapshot('header.png');
// With options
expect(await page.screenshot({
fullPage: true,
mask: [page.locator('.dynamic-content')],
})).toMatchSnapshot('page-masked.png');
// Update snapshots
// Run with: npx playwright test --update-snapshots
});Use Cases:
Reusable test setup and teardown.
import { test as base, expect } from '@playwright/test';
// Define custom fixture
const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Setup
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForNavigation();
// Use fixture
await use(page);
// Teardown
await page.goto('https://example.com/logout');
},
apiClient: async ({}, use) => {
const client = {
async get(url) {
const response = await fetch(url);
return response.json();
},
};
await use(client);
},
});
test('using fixtures', async ({ authenticatedPage, apiClient }) => {
// authenticatedPage is already logged in
await authenticatedPage.goto('https://example.com/dashboard');
expect(authenticatedPage).toHaveURL('**/dashboard');
// Use API client
const data = await apiClient.get('https://api.example.com/user');
expect(data).toHaveProperty('id');
});
export { test, expect };Use Cases:
Powerful debugging tools for test failures.
import { test, expect } from '@playwright/test';
test('debugging', async ({ page }) => {
// Enable trace recording
await page.context().tracing.start({ screenshots: true, snapshots: true });
await page.goto('https://example.com');
await page.click('button');
// Stop and save trace
await page.context().tracing.stop({ path: 'trace.zip' });
// Pause execution for debugging
// await page.pause();
// Log information
console.log('Current URL:', page.url());
console.log('Page title:', await page.title());
// Get page content
const content = await page.content();
console.log('HTML:', content);
});Use Cases:
Run tests efficiently across multiple workers.
import { test, expect } from '@playwright/test';
// Configure parallel execution
export const config = {
fullyParallel: true,
workers: 4,
retries: 2,
timeout: 30000,
};
test.describe.parallel('parallel tests', () => {
test('test 1', async ({ page }) => {
await page.goto('https://example.com');
});
test('test 2', async ({ page }) => {
await page.goto('https://example.com');
});
test('test 3', async ({ page }) => {
await page.goto('https://example.com');
});
});
// Sharding for CI/CD
// Run with: npx playwright test --shard=1/3Use Cases:
Now let's build a production-ready e-commerce web application with React that we'll test comprehensively. The application includes:
npx create-react-app ecommerce-app
cd ecommerce-app
npm install axios react-router-dom zustand
npm install -D @playwright/test
npx playwright installmkdir -p src/components src/pages src/store src/api
mkdir -p tests/e2e tests/fixturesimport axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000/api';
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to requests
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export const productApi = {
getAll: (limit = 20, offset = 0) =>
apiClient.get('/products', { params: { limit, offset } }),
getById: (id) => apiClient.get(`/products/${id}`),
search: (query) => apiClient.get('/products/search', { params: { q: query } }),
};
export const cartApi = {
getCart: () => apiClient.get('/cart'),
addItem: (productId, quantity) =>
apiClient.post('/cart/add', { productId, quantity }),
removeItem: (productId) => apiClient.delete(`/cart/items/${productId}`),
updateQuantity: (productId, quantity) =>
apiClient.put(`/cart/items/${productId}`, { quantity }),
clear: () => apiClient.post('/cart/clear'),
};
export const orderApi = {
create: (items) => apiClient.post('/orders', { items }),
getById: (id) => apiClient.get(`/orders/${id}`),
getAll: () => apiClient.get('/orders'),
processPayment: (orderId, paymentDetails) =>
apiClient.post(`/orders/${orderId}/payment`, paymentDetails),
};
export const authApi = {
login: (email, password) =>
apiClient.post('/auth/login', { email, password }),
register: (email, password, name) =>
apiClient.post('/auth/register', { email, password, name }),
logout: () => apiClient.post('/auth/logout'),
getCurrentUser: () => apiClient.get('/auth/me'),
};import { create } from 'zustand';
interface CartItem {
productId: number;
quantity: number;
price: number;
}
interface User {
id: string;
email: string;
name: string;
}
interface Store {
user: User | null;
cart: CartItem[];
isLoading: boolean;
error: string | null;
setUser: (user: User | null) => void;
setCart: (cart: CartItem[]) => void;
addToCart: (item: CartItem) => void;
removeFromCart: (productId: number) => void;
clearCart: () => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
}
export const useStore = create<Store>((set) => ({
user: null,
cart: [],
isLoading: false,
error: null,
setUser: (user) => set({ user }),
setCart: (cart) => set({ cart }),
addToCart: (item) =>
set((state) => {
const existing = state.cart.find((i) => i.productId === item.productId);
if (existing) {
return {
cart: state.cart.map((i) =>
i.productId === item.productId
? { ...i, quantity: i.quantity + item.quantity }
: i
),
};
}
return { cart: [...state.cart, item] };
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((i) => i.productId !== productId),
})),
clearCart: () => set({ cart: [] }),
setLoading: (loading) => set({ isLoading: loading }),
setError: (error) => set({ error }),
}));import React from 'react';
interface Product {
id: number;
name: string;
price: number;
description: string;
image: string;
}
interface ProductCardProps {
product: Product;
onAddToCart: (product: Product) => void;
}
export const ProductCard: React.FC<ProductCardProps> = ({
product,
onAddToCart,
}) => {
return (
<div className="product-card" data-testid={`product-${product.id}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="description">{product.description}</p>
<div className="price">${product.price.toFixed(2)}</div>
<button
onClick={() => onAddToCart(product)}
data-testid={`add-to-cart-${product.id}`}
>
Add to Cart
</button>
</div>
);
};import React from 'react';
import { useStore } from '../store/useStore';
export const Cart: React.FC = () => {
const { cart, removeFromCart } = useStore();
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
<div className="cart" data-testid="cart">
<h2>Shopping Cart</h2>
{cart.length === 0 ? (
<p data-testid="empty-cart">Your cart is empty</p>
) : (
<>
<ul>
{cart.map((item) => (
<li key={item.productId} data-testid={`cart-item-${item.productId}`}>
<span>Product {item.productId}</span>
<span>Qty: {item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button
onClick={() => removeFromCart(item.productId)}
data-testid={`remove-${item.productId}`}
>
Remove
</button>
</li>
))}
</ul>
<div className="total" data-testid="cart-total">
Total: ${total.toFixed(2)}
</div>
<button className="checkout-btn" data-testid="checkout-btn">
Proceed to Checkout
</button>
</>
)}
</div>
);
};import React, { useEffect, useState } from 'react';
import { productApi } from '../api/client';
import { ProductCard } from '../components/ProductCard';
import { useStore } from '../store/useStore';
interface Product {
id: number;
name: string;
price: number;
description: string;
image: string;
}
export const ProductsPage: React.FC = () => {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const { addToCart } = useStore();
useEffect(() => {
const fetchProducts = async () => {
try {
const response = await productApi.getAll();
setProducts(response.data.products);
} catch (error) {
console.error('Failed to fetch products', error);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
if (loading) return <div data-testid="loading">Loading...</div>;
return (
<div className="products-page" data-testid="products-page">
<h1>Products</h1>
<div className="products-grid">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={(p) => addToCart({ productId: p.id, quantity: 1, price: p.price })}
/>
))}
</div>
</div>
);
};import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { orderApi } from '../api/client';
import { useStore } from '../store/useStore';
export const CheckoutPage: React.FC = () => {
const navigate = useNavigate();
const { cart, clearCart } = useStore();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
email: '',
cardNumber: '',
expiryDate: '',
cvv: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await orderApi.create(cart);
const orderId = response.data.id;
await orderApi.processPayment(orderId, {
cardNumber: formData.cardNumber,
expiryDate: formData.expiryDate,
cvv: formData.cvv,
});
clearCart();
navigate(`/order-confirmation/${orderId}`);
} catch (error) {
console.error('Checkout failed', error);
} finally {
setLoading(false);
}
};
return (
<div className="checkout-page" data-testid="checkout-page">
<h1>Checkout</h1>
<form onSubmit={handleSubmit} data-testid="checkout-form">
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
data-testid="email-input"
required
/>
<input
type="text"
placeholder="Card Number"
value={formData.cardNumber}
onChange={(e) => setFormData({ ...formData, cardNumber: e.target.value })}
data-testid="card-input"
required
/>
<input
type="text"
placeholder="MM/YY"
value={formData.expiryDate}
onChange={(e) => setFormData({ ...formData, expiryDate: e.target.value })}
data-testid="expiry-input"
required
/>
<input
type="text"
placeholder="CVV"
value={formData.cvv}
onChange={(e) => setFormData({ ...formData, cvv: e.target.value })}
data-testid="cvv-input"
required
/>
<button type="submit" disabled={loading} data-testid="submit-btn">
{loading ? 'Processing...' : 'Complete Purchase'}
</button>
</form>
</div>
);
};import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
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'] },
},
],
webServer: {
command: 'npm start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});import { test as base, expect } from '@playwright/test';
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Navigate to login
await page.goto('/login');
// Fill login form
await page.fill('input[data-testid="email-input"]', 'user@example.com');
await page.fill('input[data-testid="password-input"]', 'password123');
// Submit form
await page.click('button[data-testid="login-btn"]');
// Wait for navigation to dashboard
await page.waitForURL('/dashboard');
// Use the authenticated page
await use(page);
// Logout
await page.click('button[data-testid="logout-btn"]');
},
});
export { expect };import { test, expect } from '@playwright/test';
test.describe('Products Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display products list', async ({ page }) => {
// Wait for products to load
await expect(page.locator('[data-testid="products-page"]')).toBeVisible();
// Check products are displayed
const products = page.locator('[data-testid^="product-"]');
const count = await products.count();
expect(count).toBeGreaterThan(0);
});
test('should add product to cart', async ({ page }) => {
// Click add to cart button
await page.click('[data-testid="add-to-cart-1"]');
// Verify cart updated
const cartBadge = page.locator('[data-testid="cart-badge"]');
await expect(cartBadge).toContainText('1');
});
test('should filter products by search', async ({ page }) => {
// Type in search
await page.fill('[data-testid="search-input"]', 'laptop');
// Wait for results
await page.waitForTimeout(500);
// Verify filtered results
const products = page.locator('[data-testid^="product-"]');
const count = await products.count();
expect(count).toBeGreaterThan(0);
});
test('should display product details', async ({ page }) => {
// Click on product
await page.click('[data-testid="product-1"]');
// Wait for details page
await page.waitForURL('**/products/1');
// Verify details displayed
await expect(page.locator('[data-testid="product-name"]')).toBeVisible();
await expect(page.locator('[data-testid="product-price"]')).toBeVisible();
await expect(page.locator('[data-testid="product-description"]')).toBeVisible();
});
});import { test, expect } from '@playwright/test';
test.describe('Shopping Cart', () => {
test('should add multiple items to cart', async ({ page }) => {
await page.goto('/');
// Add first product
await page.click('[data-testid="add-to-cart-1"]');
await expect(page.locator('[data-testid="cart-badge"]')).toContainText('1');
// Add second product
await page.click('[data-testid="add-to-cart-2"]');
await expect(page.locator('[data-testid="cart-badge"]')).toContainText('2');
// Navigate to cart
await page.click('[data-testid="cart-link"]');
// Verify items in cart
await expect(page.locator('[data-testid="cart-item-1"]')).toBeVisible();
await expect(page.locator('[data-testid="cart-item-2"]')).toBeVisible();
});
test('should remove item from cart', async ({ page }) => {
await page.goto('/');
// Add product
await page.click('[data-testid="add-to-cart-1"]');
// Go to cart
await page.click('[data-testid="cart-link"]');
// Remove item
await page.click('[data-testid="remove-1"]');
// Verify empty cart message
await expect(page.locator('[data-testid="empty-cart"]')).toBeVisible();
});
test('should update item quantity', async ({ page }) => {
await page.goto('/');
// Add product
await page.click('[data-testid="add-to-cart-1"]');
// Go to cart
await page.click('[data-testid="cart-link"]');
// Update quantity
await page.fill('[data-testid="quantity-1"]', '5');
// Verify total updated
const total = await page.locator('[data-testid="cart-total"]').textContent();
expect(total).toContain('$');
});
test('should calculate correct total', async ({ page }) => {
await page.goto('/');
// Add products with known prices
await page.click('[data-testid="add-to-cart-1"]'); // $100
await page.click('[data-testid="add-to-cart-2"]'); // $50
// Go to cart
await page.click('[data-testid="cart-link"]');
// Verify total
await expect(page.locator('[data-testid="cart-total"]')).toContainText('$150');
});
});import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
// Add items to cart
await page.goto('/');
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="add-to-cart-2"]');
});
test('should complete checkout successfully', async ({ page }) => {
// Go to cart
await page.click('[data-testid="cart-link"]');
// Click checkout
await page.click('[data-testid="checkout-btn"]');
// Wait for checkout page
await page.waitForURL('**/checkout');
// Fill form
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="card-input"]', '4111111111111111');
await page.fill('[data-testid="expiry-input"]', '12/25');
await page.fill('[data-testid="cvv-input"]', '123');
// Submit
await page.click('[data-testid="submit-btn"]');
// Wait for confirmation
await page.waitForURL('**/order-confirmation/**');
// Verify confirmation
await expect(page.locator('[data-testid="confirmation-message"]')).toContainText('Thank you');
});
test('should validate required fields', async ({ page }) => {
// Go to cart
await page.click('[data-testid="cart-link"]');
// Click checkout
await page.click('[data-testid="checkout-btn"]');
// Try to submit empty form
await page.click('[data-testid="submit-btn"]');
// Verify validation errors
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
});
test('should handle payment errors', async ({ page }) => {
// Mock payment failure
await page.route('**/api/orders/*/payment', (route) => {
route.abort('failed');
});
// Go to cart
await page.click('[data-testid="cart-link"]');
// Click checkout
await page.click('[data-testid="checkout-btn"]');
// Fill form
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="card-input"]', '4111111111111111');
await page.fill('[data-testid="expiry-input"]', '12/25');
await page.fill('[data-testid="cvv-input"]', '123');
// Submit
await page.click('[data-testid="submit-btn"]');
// Verify error message
await expect(page.locator('[data-testid="error-message"]')).toContainText('Payment failed');
});
});import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('homepage should match snapshot', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(await page.screenshot()).toMatchSnapshot('homepage.png');
});
test('products page should match snapshot', async ({ page }) => {
await page.goto('/products');
await page.waitForLoadState('networkidle');
expect(await page.screenshot()).toMatchSnapshot('products-page.png');
});
test('cart should match snapshot', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="cart-link"]');
expect(await page.screenshot()).toMatchSnapshot('cart-page.png');
});
test('checkout form should match snapshot', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="cart-link"]');
await page.click('[data-testid="checkout-btn"]');
expect(await page.screenshot()).toMatchSnapshot('checkout-page.png');
});
});# Install dependencies
npm install
# Run all tests
npx playwright test
# Run specific test file
npx playwright test tests/e2e/products.spec.ts
# Run tests in headed mode (see browser)
npx playwright test --headed
# Run tests in debug mode
npx playwright test --debug
# Run tests with UI mode
npx playwright test --ui
# Run tests for specific browser
npx playwright test --project=chromium
# Run tests in parallel
npx playwright test --workers=4
# Update visual snapshots
npx playwright test --update-snapshots
# Generate HTML report
npx playwright show-report// ❌ Wrong - unreliable
await page.waitForTimeout(2000);
await page.click('button');
// ✅ Correct - wait for element
await page.waitForSelector('button');
await page.click('button');
// ✅ Better - use locators with auto-waiting
await page.locator('button').click();// ❌ Wrong - breaks with UI changes
await page.click('div > div > button:nth-child(3)');
// ✅ Correct - semantic selectors
await page.getByRole('button', { name: 'Submit' }).click();
// ✅ Good - data-testid
await page.click('[data-testid="submit-btn"]');// ❌ Wrong - race condition
await page.click('a[href="/next"]');
await page.locator('h1').click(); // May fail if navigation not complete
// ✅ Correct - wait for navigation
await Promise.all([
page.waitForNavigation(),
page.click('a[href="/next"]'),
]);
await page.locator('h1').click();// ❌ Wrong - tests depend on each other
test('login', async ({ page }) => {
await page.goto('/login');
// ... login code
});
test('dashboard', async ({ page }) => {
// Assumes previous test logged in
await page.goto('/dashboard');
});
// ✅ Correct - each test is independent
test('dashboard after login', async ({ page }) => {
await page.goto('/login');
// ... login code
await page.goto('/dashboard');
// ... assertions
});// ❌ Wrong - depends on external service
test('checkout', async ({ page }) => {
await page.goto('/checkout');
await page.click('button[type="submit"]');
// Test fails if payment service is down
});
// ✅ Correct - mock API
test('checkout', async ({ page }) => {
await page.route('**/api/payment', (route) => {
route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
});
await page.goto('/checkout');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('**/confirmation');
});Prefer role-based and label-based locators over CSS selectors.
// ✅ Best - semantic
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('user@example.com');
await page.getByPlaceholder('Enter password').fill('password');
// Good - data-testid
await page.locator('[data-testid="submit-btn"]').click();
// Avoid - fragile selectors
await page.locator('div > button:nth-child(3)').click();Group related tests and use descriptive names.
test.describe('User Authentication', () => {
test.describe('Login', () => {
test('should login with valid credentials', async ({ page }) => {
// ...
});
test('should show error with invalid credentials', async ({ page }) => {
// ...
});
});
test.describe('Registration', () => {
test('should register new user', async ({ page }) => {
// ...
});
});
});Reuse common setup logic with fixtures.
const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Setup
await loginUser(page);
await use(page);
// Teardown
await logoutUser(page);
},
});
test('dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
// Test authenticated page
});Isolate tests by mocking APIs and external services.
test('checkout', async ({ page }) => {
// Mock payment API
await page.route('**/api/payment', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ transactionId: '12345' }),
});
});
// Mock shipping API
await page.route('**/api/shipping', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ estimatedDays: 3 }),
});
});
// Test checkout flow
await page.goto('/checkout');
// ...
});Focus on complete user journeys, not individual components.
test('complete purchase flow', async ({ page }) => {
// Browse products
await page.goto('/products');
await page.click('[data-testid="product-1"]');
// Add to cart
await page.click('[data-testid="add-to-cart"]');
// Checkout
await page.click('[data-testid="cart-link"]');
await page.click('[data-testid="checkout-btn"]');
// Complete purchase
await page.fill('[data-testid="email"]', 'user@example.com');
await page.click('[data-testid="submit"]');
// Verify confirmation
await expect(page).toHaveURL('**/confirmation');
});Catch unintended UI changes automatically.
test('homepage layout', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Compare with baseline
expect(await page.screenshot()).toMatchSnapshot('homepage.png');
});Run tests automatically in your pipeline.
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/Track test execution time and identify slow tests.
test.describe('Performance', () => {
test('homepage should load quickly', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000); // 3 seconds
});
test('search should respond quickly', async ({ page }) => {
await page.goto('/');
const startTime = Date.now();
await page.fill('[data-testid="search"]', 'laptop');
await page.waitForSelector('[data-testid="results"]');
const responseTime = Date.now() - startTime;
expect(responseTime).toBeLessThan(1000); // 1 second
});
});| Feature | Playwright | Cypress | Selenium | Puppeteer |
|---|---|---|---|---|
| Browsers | Chrome, Firefox, Safari, Edge | Chrome, Firefox, Edge | All | Chrome, Edge |
| Language | JS, Python, Java, C# | JavaScript | Multiple | JavaScript |
| Speed | Very Fast | Fast | Slow | Fast |
| Debugging | Excellent | Good | Limited | Good |
| Visual Testing | Built-in | Plugin | No | No |
| Network Control | Excellent | Good | Limited | Excellent |
| Learning Curve | Easy | Easy | Steep | Medium |
| CI/CD | Excellent | Good | Good | Good |
Choose Playwright when:
Choose Cypress when:
Choose Selenium when:
Playwright transforms frontend testing from manual, error-prone work to automated, reliable validation. Understanding E2E testing fundamentals—locators, assertions, waiting, and mocking—enables you to build comprehensive test suites.
The e-commerce application example demonstrates production patterns:
Key takeaways:
Next steps:
Playwright makes E2E testing accessible and practical. Master it, and you'll build applications with confidence, knowing that user workflows are tested and validated automatically.