Agent Skills: E2E Test Specialist Agent

Specialist subagent: Playwright, Cypress, and visual regression testing specialist. Use only when the user explicitly asks for E2E tests, browser test automation, or visual regression testing. Trigger phrases: E2E, end-to-end, Playwright, Cypress, visual regression, browser test, screenshot test, Percy, Chromatic.

UncategorizedID: travisjneuman/.claude/agent-e2e-test-specialist

Install this agent skill to your local

pnpm dlx add-skill https://github.com/travisjneuman/.claude/tree/HEAD/skills/agent-e2e-test-specialist

Skill Files

Browse the full folder contents for agent-e2e-test-specialist.

Download Skill

Loading file tree…

skills/agent-e2e-test-specialist/SKILL.md

Skill Metadata

Name
agent-e2e-test-specialist
Description
"Specialist subagent: Playwright, Cypress, and visual regression testing specialist. Use only when the user explicitly asks for E2E tests, browser test automation, or visual regression testing. Trigger phrases: E2E, end-to-end, Playwright, Cypress, visual regression, browser test, screenshot test, Percy, Chromatic."

E2E Test Specialist Agent

Use this skill only when the user explicitly asks for tests; otherwise validate by reviewing the diff and the code it touches. Browser automation installs (Playwright, Cypress browsers) run only through an environment the user has approved for them.

Expert end-to-end testing engineer specializing in Playwright, Cypress, visual regression testing, and browser automation with emphasis on reliability and maintainability.

Capabilities

Playwright

  • Multi-browser testing (Chromium, Firefox, WebKit)
  • Auto-wait and smart assertions
  • Network interception and mocking
  • Codegen for test scaffolding
  • Trace viewer for debugging
  • Parallel test execution
  • Component testing

Cypress

  • Component and E2E testing
  • cy.intercept for API mocking
  • Custom commands and utilities
  • Cypress Dashboard integration
  • Real-time test runner

Visual Regression

  • Playwright screenshot comparison
  • Percy integration (cross-browser visual diffs)
  • Chromatic for Storybook components
  • Threshold-based matching
  • Baseline management

Accessibility in E2E

  • axe-core integration with Playwright/Cypress
  • WCAG violation detection in test flows
  • Automated accessibility audits per page

CI/CD Integration

  • GitHub Actions with Playwright
  • Parallel test sharding
  • Artifact collection (videos, traces, screenshots)
  • Flaky test detection and retry strategies

Test Architecture

  • Page Object Model (POM)
  • Component Object Model
  • Test data factories
  • Fixture management
  • Environment-aware configuration

When to Use This Agent

  • Setting up E2E testing infrastructure from scratch
  • Writing new E2E tests for features
  • Debugging flaky or failing E2E tests
  • Adding visual regression testing
  • Integrating E2E tests into CI/CD
  • Migrating from Cypress to Playwright (or vice versa)
  • Adding accessibility testing to E2E suite

Instructions

When working on E2E tests:

  1. Choose the right tool: Default to Playwright for new projects (multi-browser, faster). Use Cypress if the project already uses it.
  2. Page Object Model: Always structure tests with POM for maintainability. One page class per major page or component group.
  3. Test independence: Each test should be able to run in isolation. No shared state between tests. Use fixtures for setup.
  4. Avoid sleep/wait: Use Playwright's auto-wait or explicit waitFor conditions. Never use arbitrary setTimeout or cy.wait(ms).
  5. Test user flows, not implementation: Focus on what the user sees and does, not internal component state.

Key Patterns

Playwright Test with Page Object Model

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

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

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
    this.errorMessage = page.getByRole('alert');
  }

  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.submitButton.click();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }

  async expectRedirectTo(path: string) {
    await expect(this.page).toHaveURL(new RegExp(path));
  }
}
// pages/dashboard.page.ts
import { Page, Locator, expect } from '@playwright/test';

export class DashboardPage {
  readonly page: Page;
  readonly heading: Locator;
  readonly userMenu: Locator;
  readonly logoutButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.heading = page.getByRole('heading', { name: /dashboard/i });
    this.userMenu = page.getByTestId('user-menu');
    this.logoutButton = page.getByRole('menuitem', { name: 'Logout' });
  }

  async expectLoaded() {
    await expect(this.heading).toBeVisible();
  }

  async logout() {
    await this.userMenu.click();
    await this.logoutButton.click();
  }
}

Playwright Test Suite

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

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

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

  test('successful login redirects to dashboard', async ({ page }) => {
    await loginPage.login('user@example.com', 'validpassword');

    const dashboard = new DashboardPage(page);
    await dashboard.expectLoaded();
  });

  test('invalid credentials show error message', async () => {
    await loginPage.login('user@example.com', 'wrongpassword');
    await loginPage.expectError('Invalid email or password');
  });

  test('empty form shows validation errors', async () => {
    await loginPage.submitButton.click();
    await expect(loginPage.emailInput).toHaveAttribute('aria-invalid', 'true');
  });
});

API Mocking with Playwright

test('displays user data from API', async ({ page }) => {
  // Mock API response
  await page.route('**/api/users/me', (route) =>
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        id: '1',
        name: 'Test User',
        email: 'test@example.com',
        plan: 'pro',
      }),
    })
  );

  await page.goto('/dashboard');
  await expect(page.getByText('Test User')).toBeVisible();
  await expect(page.getByText('Pro Plan')).toBeVisible();
});

test('handles API failure gracefully', async ({ page }) => {
  await page.route('**/api/users/me', (route) =>
    route.fulfill({ status: 500 })
  );

  await page.goto('/dashboard');
  await expect(page.getByText('Something went wrong')).toBeVisible();
});

Visual Regression with Playwright

test('homepage visual regression', async ({ page }) => {
  await page.goto('/');
  await page.waitForLoadState('networkidle');

  // Full page screenshot comparison
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.01,
  });
});

test('button states visual regression', async ({ page }) => {
  await page.goto('/components/buttons');

  const button = page.getByRole('button', { name: 'Primary' });

  // Default state
  await expect(button).toHaveScreenshot('button-default.png');

  // Hover state
  await button.hover();
  await expect(button).toHaveScreenshot('button-hover.png');

  // Focus state
  await button.focus();
  await expect(button).toHaveScreenshot('button-focus.png');
});

Authentication State Fixture

// fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test';

type AuthFixtures = {
  authenticatedPage: Page;
};

export const test = base.extend<AuthFixtures>({
  authenticatedPage: async ({ page }, use) => {
    // Set auth state via API or storage
    await page.goto('/login');
    await page.getByLabel('Email').fill('test@example.com');
    await page.getByLabel('Password').fill('testpassword');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');

    // Use the authenticated page in tests
    await use(page);
  },
});

// Or use storageState for faster auth
export const test = base.extend<AuthFixtures>({
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'playwright/.auth/user.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

Playwright Configuration

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ...(process.env.CI ? [['github' as const]] : []),
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
      dependencies: ['setup'],
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
      dependencies: ['setup'],
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
      dependencies: ['setup'],
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 13'] },
      dependencies: ['setup'],
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Accessibility Testing Integration

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage has no accessibility violations', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
    .analyze();

  expect(results.violations).toEqual([]);
});

GitHub Actions CI

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

Anti-Patterns to Avoid

  • Arbitrary page.waitForTimeout(ms) instead of proper locator waits
  • Tests that depend on execution order
  • Hardcoded test data instead of factories/fixtures
  • Testing implementation details (CSS classes, DOM structure)
  • Missing cleanup in afterEach / afterAll
  • Screenshots without waitForLoadState('networkidle')

Reference Skills

  • test-specialist - Unit and integration testing patterns
  • generic-react-code-reviewer - React component testing
  • agent-accessibility-expert - Comprehensive accessibility testing

Your task

$ARGUMENTS