Back to Skills

e2e-test-writer

matteocervelli
Updated Yesterday
15 views
10
10
View on GitHub
Metatestingautomation

About

This Claude skill helps developers write comprehensive Playwright end-to-end tests using the page object model pattern. It's designed for creating browser-based tests that validate user workflows and interactions across devices. The skill includes Playwright MCP tools for navigation, interaction, and screenshots to support robust test automation.

Documentation

E2E Test Writer Skill

Purpose

This skill provides comprehensive guidance for writing end-to-end tests using Playwright, following best practices including page object model pattern, proper test isolation, and maintainable test architecture.

When to Use

  • Implementing E2E tests for new features
  • Testing user workflows and journeys
  • Validating browser interactions
  • Testing responsive design across devices
  • Regression testing after changes
  • CI/CD test automation

E2E Testing Workflow

1. Setup Playwright Project

Initialize Playwright:

# Install Playwright
npm init playwright@latest

# Or add to existing project
npm install -D @playwright/test
npx playwright install

Project Structure:

tests/
├── e2e/
│   ├── auth/
│   ├── features/
│   └── workflows/
├── pages/
│   ├── BasePage.ts
│   ├── LoginPage.ts
│   └── DashboardPage.ts
├── fixtures/
│   ├── test-data.ts
│   └── custom-fixtures.ts
├── utils/
│   ├── helpers.ts
│   └── constants.ts
└── playwright.config.ts

Playwright Configuration:

// playwright.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'],
    ['json', { outputFile: 'test-results/results.json' }],
    ['junit', { outputFile: 'test-results/junit.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-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'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Deliverable: Playwright project configured and ready


2. Page Object Model Pattern

Base Page Class:

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

export class BasePage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async goto(path: string) {
    await this.page.goto(path);
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }

  async takeScreenshot(name: string) {
    await this.page.screenshot({ path: `screenshots/${name}.png` });
  }

  async getTitle(): Promise<string> {
    return await this.page.title();
  }

  async clickElement(locator: Locator) {
    await locator.waitFor({ state: 'visible' });
    await locator.click();
  }

  async fillInput(locator: Locator, value: string) {
    await locator.waitFor({ state: 'visible' });
    await locator.fill(value);
  }

  async getText(locator: Locator): Promise<string> {
    await locator.waitFor({ state: 'visible' });
    return await locator.textContent() || '';
  }
}

Example Page Object:

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

export class LoginPage extends BasePage {
  // Locators
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;
  readonly forgotPasswordLink: Locator;
  readonly signUpLink: Locator;

  constructor(page: Page) {
    super(page);
    this.usernameInput = page.locator('#username');
    this.passwordInput = page.locator('#password');
    this.submitButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('.error-message');
    this.forgotPasswordLink = page.locator('a[href="/forgot-password"]');
    this.signUpLink = page.locator('a[href="/signup"]');
  }

  // Actions
  async goto() {
    await super.goto('/login');
  }

  async login(username: string, password: string) {
    await this.fillInput(this.usernameInput, username);
    await this.fillInput(this.passwordInput, password);
    await this.clickElement(this.submitButton);
  }

  async clickForgotPassword() {
    await this.clickElement(this.forgotPasswordLink);
  }

  async clickSignUp() {
    await this.clickElement(this.signUpLink);
  }

  // Assertions helpers
  async getErrorMessage(): Promise<string> {
    return await this.getText(this.errorMessage);
  }

  async isLoginButtonEnabled(): Promise<boolean> {
    return await this.submitButton.isEnabled();
  }

  async waitForErrorMessage() {
    await this.errorMessage.waitFor({ state: 'visible' });
  }
}

Deliverable: Page objects for all application pages


3. Writing Test Cases

Basic Test Structure:

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

test.describe('User Login', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('successful login with valid credentials', async ({ page }) => {
    await loginPage.login('[email protected]', 'ValidPassword123!');

    const dashboardPage = new DashboardPage(page);
    await expect(page).toHaveURL('/dashboard');
    await expect(dashboardPage.welcomeMessage).toContainText('Welcome back');
  });

  test('failed login with invalid credentials shows error', async ({ page }) => {
    await loginPage.login('[email protected]', 'WrongPassword');

    await loginPage.waitForErrorMessage();
    const errorMsg = await loginPage.getErrorMessage();
    expect(errorMsg).toContain('Invalid username or password');
    await expect(page).toHaveURL('/login');
  });

  test('login button disabled with empty fields', async ({ page }) => {
    const isEnabled = await loginPage.isLoginButtonEnabled();
    expect(isEnabled).toBe(false);
  });

  test('forgot password link navigates correctly', async ({ page }) => {
    await loginPage.clickForgotPassword();
    await expect(page).toHaveURL('/forgot-password');
  });

  test('sign up link navigates correctly', async ({ page }) => {
    await loginPage.clickSignUp();
    await expect(page).toHaveURL('/signup');
  });
});

Testing User Workflows:

// tests/e2e/workflows/checkout.spec.ts
import { test, expect } from '@playwright/test';
import { ProductPage } from '../../pages/ProductPage';
import { CartPage } from '../../pages/CartPage';
import { CheckoutPage } from '../../pages/CheckoutPage';

test.describe('Checkout Workflow', () => {
  test('complete purchase from product to confirmation', async ({ page }) => {
    // 1. Browse product
    const productPage = new ProductPage(page);
    await productPage.goto('/products/item-123');
    await expect(productPage.productTitle).toBeVisible();

    // 2. Add to cart
    await productPage.addToCart();
    await expect(productPage.cartBadge).toHaveText('1');

    // 3. View cart
    await productPage.goToCart();
    const cartPage = new CartPage(page);
    await expect(cartPage.cartItems).toHaveCount(1);

    // 4. Proceed to checkout
    await cartPage.proceedToCheckout();
    const checkoutPage = new CheckoutPage(page);

    // 5. Fill shipping info
    await checkoutPage.fillShippingInfo({
      name: 'John Doe',
      address: '123 Main St',
      city: 'San Francisco',
      zip: '94102',
      country: 'US',
    });

    // 6. Fill payment info
    await checkoutPage.fillPaymentInfo({
      cardNumber: '4242424242424242',
      expiry: '12/25',
      cvv: '123',
    });

    // 7. Submit order
    await checkoutPage.submitOrder();

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

Deliverable: Comprehensive test suite covering user journeys


4. Test Data Management

Test Fixtures:

// fixtures/test-data.ts
export const testUsers = {
  validUser: {
    email: '[email protected]',
    password: 'ValidPassword123!',
  },
  adminUser: {
    email: '[email protected]',
    password: 'AdminPassword123!',
  },
  newUser: {
    email: '[email protected]',
    password: 'NewPassword123!',
    firstName: 'John',
    lastName: 'Doe',
  },
};

export const testProducts = {
  product1: {
    id: 'item-123',
    name: 'Test Product',
    price: 29.99,
  },
};

Custom Fixtures:

// fixtures/custom-fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

type MyFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  authenticatedPage: Page;
};

export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page);
    await use(dashboardPage);
  },

  authenticatedPage: async ({ page }, use) => {
    // Auto-login before test
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('[email protected]', 'ValidPassword123!');
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

export { expect } from '@playwright/test';

Using Custom Fixtures:

// tests/e2e/features/profile.spec.ts
import { test, expect } from '../../fixtures/custom-fixtures';

test.describe('User Profile', () => {
  test('user can update profile information', async ({ authenticatedPage, dashboardPage }) => {
    // Already logged in via authenticatedPage fixture
    await dashboardPage.goToProfile();

    // Test continues with authenticated context
    await dashboardPage.updateProfile({
      firstName: 'Jane',
      lastName: 'Smith',
    });

    await expect(dashboardPage.profileName).toHaveText('Jane Smith');
  });
});

Deliverable: Reusable test data and fixtures


5. Responsive Design Testing

Test Multiple Viewports:

// tests/e2e/responsive/layout.spec.ts
import { test, expect, devices } from '@playwright/test';

const viewports = [
  { name: 'Desktop', device: devices['Desktop Chrome'] },
  { name: 'Tablet', device: devices['iPad Pro'] },
  { name: 'Mobile', device: devices['iPhone 12'] },
];

viewports.forEach(({ name, device }) => {
  test.describe(`${name} Layout`, () => {
    test.use(device);

    test('navigation menu displays correctly', async ({ page }) => {
      await page.goto('/');

      if (name === 'Mobile') {
        // Mobile should show hamburger menu
        await expect(page.locator('.hamburger-menu')).toBeVisible();
        await expect(page.locator('.desktop-nav')).not.toBeVisible();
      } else {
        // Desktop/Tablet should show full navigation
        await expect(page.locator('.desktop-nav')).toBeVisible();
        await expect(page.locator('.hamburger-menu')).not.toBeVisible();
      }
    });

    test('images are responsive', async ({ page }) => {
      await page.goto('/products');

      const images = page.locator('img');
      const count = await images.count();

      for (let i = 0; i < count; i++) {
        const img = images.nth(i);
        const bbox = await img.boundingBox();
        if (bbox) {
          // Images should not overflow viewport
          expect(bbox.width).toBeLessThanOrEqual(device.viewport.width);
        }
      }
    });
  });
});

Deliverable: Tests covering responsive design


6. Visual Regression Testing

Screenshot Comparison:

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

test.describe('Visual Regression', () => {
  test('homepage matches baseline', async ({ page }) => {
    await page.goto('/');

    // Take full page screenshot
    await expect(page).toHaveScreenshot('homepage.png', {
      fullPage: true,
      maxDiffPixels: 100,
    });
  });

  test('modal dialog matches baseline', async ({ page }) => {
    await page.goto('/');
    await page.click('[data-testid="open-modal"]');

    // Screenshot of specific element
    const modal = page.locator('.modal');
    await expect(modal).toHaveScreenshot('modal.png');
  });

  test('dark mode matches baseline', async ({ page }) => {
    await page.goto('/');

    // Enable dark mode
    await page.evaluate(() => {
      document.documentElement.setAttribute('data-theme', 'dark');
    });

    await expect(page).toHaveScreenshot('homepage-dark.png', {
      fullPage: true,
    });
  });
});

Deliverable: Visual regression test suite


7. API Mocking and Network Testing

Mock API Responses:

// tests/e2e/network/api-mocking.spec.ts
import { test, expect } from '@playwright/test';

test.describe('API Mocking', () => {
  test('handles slow API response gracefully', async ({ page }) => {
    // Mock slow API
    await page.route('**/api/products', async (route) => {
      await new Promise(resolve => setTimeout(resolve, 3000));
      await route.fulfill({
        status: 200,
        body: JSON.stringify({ products: [] }),
      });
    });

    await page.goto('/products');

    // Should show loading state
    await expect(page.locator('.loading-spinner')).toBeVisible();

    // Should eventually show products
    await expect(page.locator('.product-list')).toBeVisible({ timeout: 5000 });
  });

  test('handles API error gracefully', async ({ page }) => {
    // Mock API error
    await page.route('**/api/products', (route) => {
      route.fulfill({
        status: 500,
        body: JSON.stringify({ error: 'Internal server error' }),
      });
    });

    await page.goto('/products');

    // Should show error message
    await expect(page.locator('.error-message')).toBeVisible();
    await expect(page.locator('.error-message')).toContainText('Failed to load products');
  });

  test('handles network timeout', async ({ page }) => {
    await page.route('**/api/products', (route) => {
      // Never fulfill, causing timeout
    });

    await page.goto('/products');

    // Should show timeout message
    await expect(page.locator('.timeout-message')).toBeVisible({ timeout: 10000 });
  });
});

Deliverable: Tests for API interactions and error states


8. Authentication State Management

Reuse Authentication State:

// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('[email protected]', 'ValidPassword123!');

  await page.waitForURL('/dashboard');

  // Save authentication state
  await page.context().storageState({ path: authFile });
});

Use Saved Auth State:

// playwright.config.ts
export default defineConfig({
  // ... other config

  projects: [
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

Deliverable: Efficient authentication handling


Best Practices

Test Organization:

  • Group related tests with test.describe()
  • Use meaningful test names (describe behavior, not implementation)
  • One assertion per test (or closely related assertions)
  • Isolate tests (no dependencies between tests)

Locator Strategy:

  • Prefer test IDs: page.locator('[data-testid="submit"]')
  • Use semantic selectors: page.locator('button:has-text("Submit")')
  • Avoid CSS selectors tied to styling
  • Never use XPath unless absolutely necessary

Waiting Strategy:

  • Use auto-waiting (built into Playwright)
  • Avoid fixed waits (page.waitForTimeout())
  • Use waitForLoadState() for page loads
  • Use waitFor() for specific elements

Error Handling:

  • Use try-catch for expected errors
  • Add screenshots on failure
  • Capture network logs
  • Record video on failure

Performance:

  • Run tests in parallel when possible
  • Use test.describe.configure({ mode: 'parallel' })
  • Share browser contexts when safe
  • Reuse authentication state
  • Mock slow external APIs

Maintainability:

  • Page Object Model for all pages
  • Extract common logic to helpers
  • Use constants for repeated values
  • Keep tests DRY (Don't Repeat Yourself)
  • Regular refactoring

Integration with Playwright MCP

Using MCP Tools:

// Example: Using Playwright MCP for navigation
test('navigate using MCP', async ({ page }) => {
  // Use mcp__playwright-mcp__playwright_navigate
  await page.evaluate(async () => {
    // MCP navigation call would go here
  });

  await expect(page).toHaveURL('/expected-page');
});

// Example: Using MCP for screenshots
test('capture screenshot with MCP', async ({ page }) => {
  await page.goto('/');

  // Use mcp__playwright-mcp__playwright_screenshot
  await page.evaluate(async () => {
    // MCP screenshot call would go here
  });
});

Remember

  • Test behavior, not implementation: Tests should survive refactoring
  • User perspective: Test what users do, not how code works
  • Isolation: Each test should run independently
  • Fast feedback: Keep tests fast (< 5 min total suite)
  • Flake-free: No intermittent failures
  • Clear failures: Easy to debug when tests fail
  • Maintainable: Easy to update when app changes
  • Comprehensive: Cover happy paths and edge cases
  • CI/CD ready: Tests run reliably in CI environment

Your goal is to create robust, maintainable E2E tests that provide confidence in application functionality across browsers and devices.

Quick Install

/plugin add https://github.com/matteocervelli/llms/tree/main/e2e-test-writer

Copy and paste this command in Claude Code to install this skill

GitHub 仓库

matteocervelli/llms
Path: .claude/skills/e2e-test-writer

Related Skills

sglang

Meta

SGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.

View skill

evaluating-llms-harness

Testing

This Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.

View skill

Algorithmic Art Generation

Meta

This skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.

View skill

webapp-testing

Testing

This Claude Skill provides a Playwright-based toolkit for testing local web applications through Python scripts. It enables frontend verification, UI debugging, screenshot capture, and log viewing while managing server lifecycles. Use it for browser automation tasks but run scripts directly rather than reading their source code to avoid context pollution.

View skill