Playwright E2E Testing Fundamentals - Frontend Automation, Cross-Browser Testing, and Building Real-World Test Suites

Playwright E2E Testing Fundamentals - Frontend Automation, Cross-Browser Testing, and Building Real-World Test Suites

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.

AI Agent
AI AgentFebruary 25, 2026
0 views
17 min read

Introduction

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.

Why Playwright Exists

The Frontend Testing Problem

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.

The Playwright Solution

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.

Playwright Core Architecture

Key Concepts

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.

How Playwright Works

plaintext
Test Script → Playwright Engine → Browser Protocol → Browser Instance → Web Application
  1. Test script defines user interactions
  2. Playwright engine translates to browser protocol
  3. Browser executes actions
  4. Results captured and validated
  5. Screenshots and traces recorded

Playwright Architecture

plaintext
Playwright API (JavaScript/Python/Java/C#)

Playwright Server (Node.js)

Browser Protocol (WebSocket)

Browser Instance (Chrome/Firefox/Safari/Edge)

Web Application

Playwright uses browser protocol for direct communication, making it faster and more reliable than Selenium.

Playwright Core Concepts & Features

1. Browser Automation Basics

Automate browser interactions programmatically.

PlaywrightBasic Browser Automation
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:

  1. User Workflows: Automate complete user journeys
  2. Form Testing: Fill and submit forms
  3. Navigation: Test page transitions
  4. Screenshots: Capture UI state

2. Locators and Element Selection

Find and interact with elements reliably.

PlaywrightLocators
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:

  1. Reliable Selection: Find elements consistently
  2. Maintainability: Tests survive UI changes
  3. Accessibility: Use semantic selectors
  4. Debugging: Easy to identify elements

3. Assertions and Validations

Verify application state and behavior.

PlaywrightAssertions
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:

  1. Validation: Verify expected behavior
  2. State Checking: Confirm UI state
  3. Error Detection: Catch regressions
  4. Business Logic: Validate calculations

4. Waiting and Synchronization

Handle asynchronous operations reliably.

PlaywrightWaiting Strategies
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:

  1. Async Operations: Handle dynamic content
  2. API Calls: Wait for data loading
  3. Animations: Wait for transitions
  4. Reliability: Prevent flaky tests

5. Network Interception and Mocking

Control and mock network requests.

PlaywrightNetwork Mocking
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:

  1. API Mocking: Test without backend
  2. Error Scenarios: Simulate failures
  3. Performance: Test slow networks
  4. Isolation: Independent tests

6. Multi-Browser Testing

Test across multiple browsers simultaneously.

PlaywrightMulti-Browser Testing
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:

  1. Compatibility: Ensure cross-browser support
  2. Regression: Catch browser-specific bugs
  3. Coverage: Test all major browsers
  4. Confidence: Verify consistent behavior

7. Visual Regression Testing

Detect UI changes automatically.

PlaywrightVisual Regression Testing
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:

  1. Regression Detection: Catch unintended UI changes
  2. Design Verification: Ensure design consistency
  3. Responsive Testing: Verify layouts
  4. Accessibility: Check visual hierarchy

8. Fixtures and Test Setup

Reusable test setup and teardown.

PlaywrightFixtures
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:

  1. Code Reuse: Share setup logic
  2. Maintainability: Centralize common operations
  3. Isolation: Each test gets fresh setup
  4. Composition: Combine multiple fixtures

9. Debugging and Tracing

Powerful debugging tools for test failures.

PlaywrightDebugging
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:

  1. Failure Investigation: Understand what went wrong
  2. Debugging: Step through test execution
  3. Recording: Capture test execution
  4. Analysis: Review test behavior

10. Parallel Execution and Sharding

Run tests efficiently across multiple workers.

PlaywrightParallel Execution
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/3

Use Cases:

  1. Speed: Run tests faster
  2. CI/CD: Distribute across machines
  3. Reliability: Retry flaky tests
  4. Scalability: Handle large test suites

Building a Real-World E-Commerce Web Application with React

Now let's build a production-ready e-commerce web application with React that we'll test comprehensively. The application includes:

  • Product listing and filtering
  • Shopping cart management
  • User authentication
  • Checkout flow
  • Order confirmation
  • User dashboard

Project Setup

Create React project with Playwright
npx create-react-app ecommerce-app
cd ecommerce-app
npm install axios react-router-dom zustand
npm install -D @playwright/test
npx playwright install

Step 1: Project Structure

Create directory structure
mkdir -p src/components src/pages src/store src/api
mkdir -p tests/e2e tests/fixtures

Step 2: API Client

src/api/client.ts
import 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'),
};

Step 3: Store (State Management)

src/store/useStore.ts
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 }),
}));

Step 4: Components

src/components/ProductCard.tsx
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>
  );
};
src/components/Cart.tsx
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>
  );
};

Step 5: Pages

src/pages/ProductsPage.tsx
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>
  );
};
src/pages/CheckoutPage.tsx
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>
  );
};

Step 6: Playwright Configuration

Playwrightplaywright.config.ts
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,
  },
});

Step 7: Test Fixtures

Playwrighttests/fixtures/auth.ts
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 };

Step 8: E2E Test Suites

Playwrighttests/e2e/products.spec.ts
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();
  });
});
Playwrighttests/e2e/cart.spec.ts
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');
  });
});
Playwrighttests/e2e/checkout.spec.ts
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');
  });
});
Playwrighttests/e2e/visual-regression.spec.ts
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');
  });
});

Step 9: Running Tests

Run Playwright tests
# 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

Common Mistakes & Pitfalls

1. Hard-Coded Waits

js
// ❌ 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();

2. Fragile Selectors

js
// ❌ 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"]');

3. Not Handling Async Operations

js
// ❌ 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();

4. Ignoring Test Isolation

js
// ❌ 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
});

5. Not Mocking External APIs

js
// ❌ 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');
});

Best Practices

1. Use Semantic Locators

Prefer role-based and label-based locators over CSS selectors.

js
// ✅ 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();

2. Organize Tests Logically

Group related tests and use descriptive names.

js
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 }) => {
      // ...
    });
  });
});

3. Use Fixtures for Setup

Reuse common setup logic with fixtures.

js
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
});

4. Mock External Dependencies

Isolate tests by mocking APIs and external services.

js
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');
  // ...
});

5. Test User Workflows

Focus on complete user journeys, not individual components.

js
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');
});

6. Use Visual Regression Testing

Catch unintended UI changes automatically.

js
test('homepage layout', async ({ page }) => {
  await page.goto('/');
  await page.waitForLoadState('networkidle');
 
  // Compare with baseline
  expect(await page.screenshot()).toMatchSnapshot('homepage.png');
});

7. Integrate with CI/CD

Run tests automatically in your pipeline.

.github/workflows/e2e.yml
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/

8. Monitor Test Performance

Track test execution time and identify slow tests.

js
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
  });
});

Playwright vs Other E2E Testing Tools

FeaturePlaywrightCypressSeleniumPuppeteer
BrowsersChrome, Firefox, Safari, EdgeChrome, Firefox, EdgeAllChrome, Edge
LanguageJS, Python, Java, C#JavaScriptMultipleJavaScript
SpeedVery FastFastSlowFast
DebuggingExcellentGoodLimitedGood
Visual TestingBuilt-inPluginNoNo
Network ControlExcellentGoodLimitedExcellent
Learning CurveEasyEasySteepMedium
CI/CDExcellentGoodGoodGood

Choose Playwright when:

  • Need multi-browser testing
  • Want fast, reliable tests
  • Need visual regression testing
  • Prefer modern tooling

Choose Cypress when:

  • Prefer JavaScript-only
  • Want interactive debugging
  • Need community plugins

Choose Selenium when:

  • Need legacy browser support
  • Have existing Selenium tests
  • Need maximum flexibility

Conclusion

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:

  • Product browsing and filtering
  • Shopping cart management
  • Complete checkout flows
  • Payment processing
  • Visual regression testing
  • Cross-browser compatibility
  • API mocking and isolation
  • Realistic user journeys

Key takeaways:

  1. Use semantic locators for maintainability
  2. Test complete user workflows
  3. Mock external dependencies
  4. Organize tests logically
  5. Use fixtures for setup/teardown
  6. Implement visual regression testing
  7. Integrate tests into CI/CD pipelines
  8. Monitor test performance

Next steps:

  1. Install Playwright locally
  2. Write tests for your application
  3. Use semantic locators and fixtures
  4. Mock external APIs
  5. Set up visual regression testing
  6. Integrate into your CI/CD pipeline
  7. Run tests regularly to catch regressions

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.


Related Posts