Claude Code Plugins

Community-maintained marketplace

Feedback

e2e-testing-automation

@aj-geddes/useful-ai-prompts
4
0

Build end-to-end automated tests that simulate real user interactions across the full application stack. Use for E2E test, Selenium, Cypress, Playwright, browser automation, and user journey testing.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name e2e-testing-automation
description Build end-to-end automated tests that simulate real user interactions across the full application stack. Use for E2E test, Selenium, Cypress, Playwright, browser automation, and user journey testing.

E2E Testing Automation

Overview

End-to-end (E2E) testing validates complete user workflows from the UI through all backend systems, ensuring the entire application stack works together correctly from a user's perspective. E2E tests simulate real user interactions with browsers, handling authentication, navigation, form submissions, and validating results.

When to Use

  • Testing critical user journeys (signup, checkout, login)
  • Validating multi-step workflows
  • Testing across different browsers and devices
  • Regression testing for UI changes
  • Verifying frontend-backend integration
  • Testing with real user interactions (clicks, typing, scrolling)
  • Smoke testing deployments

Instructions

1. Playwright E2E Tests

// tests/e2e/checkout.spec.ts
import { test, expect, Page } from '@playwright/test';

test.describe('E-commerce Checkout Flow', () => {
  let page: Page;

  test.beforeEach(async ({ page: p }) => {
    page = p;
    await page.goto('/');
  });

  test('complete checkout flow as guest user', async () => {
    // 1. Browse and add product to cart
    await page.click('text=Shop Now');
    await page.click('[data-testid="product-1"]');
    await expect(page.locator('h1')).toContainText('Product Name');

    await page.click('button:has-text("Add to Cart")');
    await expect(page.locator('.cart-count')).toHaveText('1');

    // 2. Go to cart and proceed to checkout
    await page.click('[data-testid="cart-icon"]');
    await expect(page.locator('.cart-item')).toHaveCount(1);
    await page.click('text=Proceed to Checkout');

    // 3. Fill shipping information
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="firstName"]', 'John');
    await page.fill('[name="lastName"]', 'Doe');
    await page.fill('[name="address"]', '123 Main St');
    await page.fill('[name="city"]', 'San Francisco');
    await page.selectOption('[name="state"]', 'CA');
    await page.fill('[name="zip"]', '94105');

    // 4. Enter payment information
    await page.click('text=Continue to Payment');

    // Wait for payment iframe to load
    const paymentFrame = page.frameLocator('iframe[name="payment-frame"]');
    await paymentFrame.locator('[name="cardNumber"]').fill('4242424242424242');
    await paymentFrame.locator('[name="expiry"]').fill('12/25');
    await paymentFrame.locator('[name="cvc"]').fill('123');

    // 5. Complete order
    await page.click('button:has-text("Place Order")');

    // 6. Verify success
    await expect(page).toHaveURL(/\/order\/confirmation/);
    await expect(page.locator('.confirmation-message')).toContainText('Order placed successfully');

    const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
    expect(orderNumber).toMatch(/^ORD-\d+$/);
  });

  test('checkout with existing user account', async () => {
    // Login first
    await page.click('text=Sign In');
    await page.fill('[name="email"]', 'existing@example.com');
    await page.fill('[name="password"]', 'Password123!');
    await page.click('button[type="submit"]');

    await expect(page.locator('.user-menu')).toContainText('existing@example.com');

    // Add product and checkout with saved information
    await page.click('[data-testid="product-2"]');
    await page.click('button:has-text("Add to Cart")');
    await page.click('[data-testid="cart-icon"]');
    await page.click('text=Checkout');

    // Verify saved address is pre-filled
    await expect(page.locator('[name="address"]')).toHaveValue(/./);

    // Complete checkout
    await page.click('button:has-text("Use Saved Payment")');
    await page.click('button:has-text("Place Order")');

    await expect(page).toHaveURL(/\/order\/confirmation/);
  });

  test('handle out of stock product', async () => {
    await page.click('[data-testid="product-out-of-stock"]');

    const addToCartButton = page.locator('button:has-text("Add to Cart")');
    await expect(addToCartButton).toBeDisabled();
    await expect(page.locator('.stock-status')).toHaveText('Out of Stock');
  });
});

2. Cypress E2E Tests

// cypress/e2e/authentication.cy.js
describe('User Authentication Flow', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('should register a new user account', () => {
    cy.get('[data-cy="signup-button"]').click();
    cy.url().should('include', '/signup');

    // Fill registration form
    const timestamp = Date.now();
    cy.get('[name="email"]').type(`user${timestamp}@example.com`);
    cy.get('[name="password"]').type('SecurePass123!');
    cy.get('[name="confirmPassword"]').type('SecurePass123!');
    cy.get('[name="firstName"]').type('Test');
    cy.get('[name="lastName"]').type('User');

    // Accept terms
    cy.get('[name="acceptTerms"]').check();

    // Submit form
    cy.get('button[type="submit"]').click();

    // Verify success
    cy.url().should('include', '/dashboard');
    cy.get('.welcome-message').should('contain', 'Welcome, Test!');

    // Verify email sent (check via API)
    cy.request(`/api/test/emails/${timestamp}@example.com`)
      .its('body')
      .should('have.property', 'subject', 'Welcome to Our App');
  });

  it('should handle validation errors', () => {
    cy.get('[data-cy="signup-button"]').click();

    // Submit empty form
    cy.get('button[type="submit"]').click();

    // Check for validation errors
    cy.get('.error-message').should('have.length.greaterThan', 0);
    cy.get('[name="email"]')
      .parent()
      .should('contain', 'Email is required');

    // Fill invalid email
    cy.get('[name="email"]').type('invalid-email');
    cy.get('[name="password"]').type('weak');
    cy.get('button[type="submit"]').click();

    cy.get('[name="email"]')
      .parent()
      .should('contain', 'Invalid email format');
    cy.get('[name="password"]')
      .parent()
      .should('contain', 'Password must be at least 8 characters');
  });

  it('should login with valid credentials', () => {
    // Create test user first
    cy.request('POST', '/api/test/users', {
      email: 'test@example.com',
      password: 'Password123!',
      name: 'Test User'
    });

    // Login
    cy.get('[data-cy="login-button"]').click();
    cy.get('[name="email"]').type('test@example.com');
    cy.get('[name="password"]').type('Password123!');
    cy.get('button[type="submit"]').click();

    // Verify login successful
    cy.url().should('include', '/dashboard');
    cy.getCookie('auth_token').should('exist');

    // Verify user menu
    cy.get('[data-cy="user-menu"]').click();
    cy.get('.user-email').should('contain', 'test@example.com');
  });

  it('should maintain session across page reloads', () => {
    // Login
    cy.loginViaAPI('test@example.com', 'Password123!');
    cy.visit('/dashboard');

    // Verify logged in
    cy.get('.user-menu').should('exist');

    // Reload page
    cy.reload();

    // Still logged in
    cy.get('.user-menu').should('exist');
    cy.getCookie('auth_token').should('exist');
  });

  it('should logout successfully', () => {
    cy.loginViaAPI('test@example.com', 'Password123!');
    cy.visit('/dashboard');

    cy.get('[data-cy="user-menu"]').click();
    cy.get('[data-cy="logout-button"]').click();

    cy.url().should('equal', Cypress.config().baseUrl + '/');
    cy.getCookie('auth_token').should('not.exist');
  });
});

// Custom command for login
Cypress.Commands.add('loginViaAPI', (email, password) => {
  cy.request('POST', '/api/auth/login', { email, password })
    .then((response) => {
      window.localStorage.setItem('auth_token', response.body.token);
    });
});

3. Selenium with Python (pytest)

# tests/e2e/test_search_functionality.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys

class TestSearchFunctionality:
    @pytest.fixture
    def driver(self):
        """Setup and teardown browser."""
        options = webdriver.ChromeOptions()
        options.add_argument('--headless')
        driver = webdriver.Chrome(options=options)
        driver.implicitly_wait(10)
        yield driver
        driver.quit()

    def test_search_with_results(self, driver):
        """Test search functionality returns relevant results."""
        driver.get('http://localhost:3000')

        # Find search box and enter query
        search_box = driver.find_element(By.NAME, 'search')
        search_box.send_keys('laptop')
        search_box.send_keys(Keys.RETURN)

        # Wait for results
        wait = WebDriverWait(driver, 10)
        results = wait.until(
            EC.presence_of_all_elements_located((By.CLASS_NAME, 'search-result'))
        )

        # Verify results
        assert len(results) > 0
        assert 'laptop' in driver.page_source.lower()

        # Check first result has required elements
        first_result = results[0]
        assert first_result.find_element(By.CLASS_NAME, 'product-title')
        assert first_result.find_element(By.CLASS_NAME, 'product-price')
        assert first_result.find_element(By.CLASS_NAME, 'product-image')

    def test_search_filters(self, driver):
        """Test applying filters to search results."""
        driver.get('http://localhost:3000/search?q=laptop')

        wait = WebDriverWait(driver, 10)

        # Wait for results to load
        wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, 'search-result'))
        )

        initial_count = len(driver.find_elements(By.CLASS_NAME, 'search-result'))

        # Apply price filter
        price_filter = driver.find_element(By.ID, 'price-filter-500-1000')
        price_filter.click()

        # Wait for filtered results
        wait.until(
            EC.staleness_of(driver.find_element(By.CLASS_NAME, 'search-result'))
        )
        wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, 'search-result'))
        )

        filtered_count = len(driver.find_elements(By.CLASS_NAME, 'search-result'))

        # Verify filter was applied
        assert filtered_count <= initial_count

        # Verify all prices are in range
        prices = driver.find_elements(By.CLASS_NAME, 'product-price')
        for price_elem in prices:
            price = float(price_elem.text.replace('$', '').replace(',', ''))
            assert 500 <= price <= 1000

    def test_pagination(self, driver):
        """Test navigating through search result pages."""
        driver.get('http://localhost:3000/search?q=electronics')

        wait = WebDriverWait(driver, 10)

        # Get first page results
        first_page_results = driver.find_elements(By.CLASS_NAME, 'search-result')
        first_result_title = first_page_results[0].find_element(
            By.CLASS_NAME, 'product-title'
        ).text

        # Click next page
        next_button = driver.find_element(By.CSS_SELECTOR, '[aria-label="Next page"]')
        next_button.click()

        # Wait for new results
        wait.until(EC.staleness_of(first_page_results[0]))

        # Verify on page 2
        assert 'page=2' in driver.current_url

        second_page_results = driver.find_elements(By.CLASS_NAME, 'search-result')
        second_result_title = second_page_results[0].find_element(
            By.CLASS_NAME, 'product-title'
        ).text

        # Results should be different
        assert first_result_title != second_result_title

    def test_empty_search_results(self, driver):
        """Test handling of searches with no results."""
        driver.get('http://localhost:3000')

        search_box = driver.find_element(By.NAME, 'search')
        search_box.send_keys('xyznonexistentproduct123')
        search_box.send_keys(Keys.RETURN)

        wait = WebDriverWait(driver, 10)
        no_results = wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, 'no-results'))
        )

        assert 'no results found' in no_results.text.lower()
        assert len(driver.find_elements(By.CLASS_NAME, 'search-result')) == 0

4. Page Object Model Pattern

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('[name="email"]');
    this.passwordInput = page.locator('[name="password"]');
    this.loginButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('.error-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 getErrorMessage(): Promise<string> {
    return await this.errorMessage.textContent();
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('login with invalid credentials', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('invalid@example.com', 'wrongpassword');

  const error = await loginPage.getErrorMessage();
  expect(error).toContain('Invalid credentials');
});

Best Practices

✅ DO

  • Use data-testid attributes for stable selectors
  • Implement Page Object Model for maintainability
  • Test critical user journeys thoroughly
  • Run tests in multiple browsers (cross-browser testing)
  • Use explicit waits instead of sleep/timeouts
  • Clean up test data after each test
  • Take screenshots on failures
  • Parallelize test execution where possible

❌ DON'T

  • Use brittle CSS selectors (like nth-child)
  • Test every possible UI combination (focus on critical paths)
  • Share state between tests
  • Use fixed delays (sleep/timeout)
  • Ignore flaky tests
  • Run E2E tests for unit-level testing
  • Test third-party UI components in detail
  • Skip mobile/responsive testing

Tools & Frameworks

  • Playwright: Modern, fast, reliable (Node.js, Python, Java, .NET)
  • Cypress: Developer-friendly, fast feedback loop (JavaScript)
  • Selenium: Cross-browser, mature ecosystem (multiple languages)
  • Puppeteer: Chrome DevTools Protocol automation (Node.js)
  • WebDriverIO: Next-gen browser automation (Node.js)

Configuration Examples

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  timeout: 30000,
  retries: 2,
  workers: process.env.CI ? 2 : 4,

  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
  },

  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],

  webServer: {
    command: 'npm run start',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

Examples

See also: integration-testing, visual-regression-testing, accessibility-testing, test-automation-framework skills.