Skip to content
Playwright Testing logo

Playwright Testing

> **Skill Purpose:** E2E test setup, component testing patterns, and responsive testing automation

Coverage-Creatives/zeus0installs0stars

SKILL.md

Full skill instructions

Playwright Testing

Skill Purpose: E2E test setup, component testing patterns, and responsive testing automation


Core Skill Pattern

Objective: Establish comprehensive E2E testing strategy with Playwright, component testing, and responsive validation.

Universal Pattern:

  1. Define testing strategy and scope
  2. Create Playwright configuration and setup
  3. Establish E2E testing patterns and procedures
  4. Set up component testing framework
  5. Create responsive and accessibility testing automation

Key Decisions (Project-Specific):

  • Testing scope and coverage requirements
  • Browser and device targets for testing
  • Test environment and data management
  • CI/​CD integration and parallel execution
  • Reporting and test result management

Project-Specific Implementation Notes

Customize per project:

  • Browser targets based on user analytics and requirements
  • Test scope based on application complexity and critical paths
  • Test data strategy based on data privacy and complexity
  • Parallel execution based on CI/​CD resources
  • Reporting approach based on team needs and compliance

Example Implementation (Next.js Playwright Pattern)

Note: This is an example pattern using Playwright with Next.js. Adapt testing framework and patterns based on your specific project requirements and testing needs.

Prerequisites (Example)

  • Next.js project structure established
  • Playwright installed and configured
  • Test environment and database setup
  • CI/​CD pipeline configured for automated testing

Example: Playwright Testing Implementation

Framework-Specific Example: This demonstrates Playwright testing patterns with Next.js. Adapt for your testing framework and application requirements.

Playwright Configuration

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

export default defineConfig({
  testDir: './​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/​results.xml' }],
  ],
  use: {
    baseURL: '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'] },
    },
    {
      name: 'Tablet',
      use: { ...devices['iPad Pro'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Test Utilities and Helpers

// e2e/​utils/​test-helpers.ts
import { expect, Page, TestInfo } from '@playwright/​test';

export class TestHelpers {
  static async login(page: Page, email: string, password: string) {
    await page.goto('/​login');
    
    await page.fill('[data-testid=email-input]', email);
    await page.fill('[data-testid=password-input]', password);
    await page.click('[data-testid=login-button]');
    
    // Wait for navigation to dashboard
    await expect(page).toHaveURL('/​dashboard');
    await expect(page.locator('[data-testid=user-menu]')).toBeVisible();
  }

  static async logout(page: Page) {
    await page.click('[data-testid=user-menu]');
    await page.click('[data-testid=logout-button]');
    
    await expect(page).toHaveURL('/​login');
  }

  static async waitForAPIResponse(page: Page, url: string) {
    return page.waitForResponse(response => 
      response.url().includes(url)
    );
  }

  static async takeScreenshot(page: Page, name: string, testInfo: TestInfo) {
    const screenshot = await page.screenshot({
      fullPage: true,
      path: `test-results/​${name}-${testInfo.retry}.png`
    });
    
    await testInfo.attachments.push({
      name: name,
      contentType: 'image/​png',
      body: screenshot
    });
  }

  static async checkAccessibility(page: Page) {
    // This would integrate with axe-core for accessibility testing
    await page.injectTag({
      path: 'node_modules/​axe-core/​axe.min.js'
    });
    
    const results = await page.evaluate(() => {
      // @ts-ignore
      return axe.run();
    });
    
    expect(results.violations).toHaveLength(0);
  }

  static async generateTestData() {
    return {
      user: {
        email: `test-${Date.now()}@example.com`,
        name: 'Test User',
        password: 'TestPassword123!',
      },
      product: {
        name: 'Test Product',
        price: 29.99,
        description: 'A test product for E2E testing',
      }
    };
  }
}

E2E Test Examples

// e2e/​auth.spec.ts
import { test, expect } from '@playwright/​test';
import { TestHelpers } from './​utils/​test-helpers';

test.describe('Authentication', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('should allow user to login with valid credentials', async ({ page }) => {
    const testData = await TestHelpers.generateTestData();
    
    // Navigate to login
    await page.click('[data-testid=login-link]');
    await expect(page).toHaveURL('/​login');

    // Fill login form
    await page.fill('[data-testid=email-input]', testData.user.email);
    await page.fill('[data-testid=password-input]', testData.user.password);
    await page.click('[data-testid=login-button]');

    // Verify successful login
    await expect(page).toHaveURL('/​dashboard');
    await expect(page.locator('[data-testid=welcome-message]')).toContainText('Welcome');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.click('[data-testid=login-link]');
    
    await page.fill('[data-testid=email-input]', '[email protected]');
    await page.fill('[data-testid=password-input]', 'wrongpassword');
    await page.click('[data-testid=login-button]');

    // Should stay on login page and show error
    await expect(page).toHaveURL('/​login');
    await expect(page.locator('[data-testid=error-message]')).toBeVisible();
    await expect(page.locator('[data-testid=error-message]')).toContainText('Invalid credentials');
  });

  test('should allow user to logout', async ({ page }) => {
    const testData = await TestHelpers.generateTestData();
    
    // First login
    await TestHelpers.login(page, testData.user.email, testData.user.password);
    
    // Then logout
    await TestHelpers.logout(page);
    
    // Verify logged out state
    await expect(page.locator('[data-testid=login-link]')).toBeVisible();
  });
});

Responsive Testing

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

const breakpoints = [
  { name: 'mobile', device: devices['iPhone 12'] },
  { name: 'tablet', device: devices['iPad Pro'] },
  { name: 'desktop', device: devices['Desktop Chrome'] },
];

breakpoints.forEach(({ name, device }) => {
  test.describe(`Responsive - ${name}`, () => {
    test.use({ ...device });

    test('should display navigation correctly', async ({ page }) => {
      await page.goto('/');
      
      if (name === 'mobile') {
        // Mobile should show hamburger menu
        await expect(page.locator('[data-testid=mobile-menu-button]')).toBeVisible();
        await expect(page.locator('[data-testid=desktop-nav]')).toBeHidden();
        
        // Test mobile menu interaction
        await page.click('[data-testid=mobile-menu-button]');
        await expect(page.locator('[data-testid=mobile-menu]')).toBeVisible();
      } else {
        // Tablet and desktop should show full navigation
        await expect(page.locator('[data-testid=desktop-nav]')).toBeVisible();
        await expect(page.locator('[data-testid=mobile-menu-button]')).toBeHidden();
      }
    });

    test('should display content correctly', async ({ page }) => {
      await page.goto('/​products');
      
      // Check that product grid adapts to screen size
      const productGrid = page.locator('[data-testid=product-grid]');
      await expect(productGrid).toBeVisible();
      
      if (name === 'mobile') {
        // Mobile should have 1 column
        await expect(productGrid.locator('[data-testid=product-card]').first()).toBeVisible();
      } else if (name === 'tablet') {
        // Tablet should have 2 columns
        const gridColumns = await productGrid.evaluate(el => 
          getComputedStyle(el).gridTemplateColumns
        );
        expect(gridColumns).toContain('repeat(2');
      } else {
        // Desktop should have 3-4 columns
        const gridColumns = await productGrid.evaluate(el => 
          getComputedStyle(el).gridTemplateColumns
        );
        expect(gridColumns).toContain('repeat(3');
      }
    });

    test('should handle touch interactions on mobile', async ({ page }) => {
      if (name !== 'mobile') return;
      
      await page.goto('/');
      
      // Test swipe gestures if applicable
      const carousel = page.locator('[data-testid=carousel]');
      if (await carousel.isVisible()) {
        await carousel.swipe('left');
        await page.waitForTimeout(500);
        
        // Verify carousel moved to next slide
        const activeSlide = carousel.locator('[data-testid=carousel-slide].active');
        await expect(activeSlide).toBeVisible();
      }
    });
  });
});

Component Testing with Playwright

// e2e/​components/​button.spec.ts
import { test, expect } from '@playwright/​test';

test.describe('Button Component', () => {
  test('should be accessible', async ({ page }) => {
    await page.goto('/​components/​button');
    
    const button = page.locator('[data-testid=test-button]');
    await expect(button).toBeVisible();
    
    // Check accessibility attributes
    await expect(button).toHaveAttribute('role', 'button');
    await expect(button).toHaveAttribute('tabindex', '0');
    
    // Test keyboard navigation
    await button.focus();
    await expect(button).toBeFocused();
    
    await page.keyboard.press('Enter');
    await expect(page.locator('[data-testid=button-click-result]')).toContainText('Button clicked');
  });

  test('should handle different states', async ({ page }) => {
    await page.goto('/​components/​button');
    
    // Test disabled state
    const disabledButton = page.locator('[data-testid=disabled-button]');
    await expect(disabledButton).toBeDisabled();
    await expect(disabledButton).toHaveCSS('opacity', '0.5');
    
    // Test loading state
    const loadingButton = page.locator('[data-testid=loading-button]');
    await loadingButton.click();
    await expect(loadingButton.locator('[data-testid=spinner]')).toBeVisible();
    await expect(loadingButton).toBeDisabled();
  });

  test('should have proper color contrast', async ({ page }) => {
    await page.goto('/​components/​button');
    
    const button = page.locator('[data-testid=test-button]');
    
    // Get computed styles for color contrast testing
    const styles = await button.evaluate(el => {
      const computed = getComputedStyle(el);
      return {
        color: computed.color,
        backgroundColor: computed.backgroundColor,
      };
    });
    
    // This would integrate with a color contrast checker
    // For now, just verify the styles exist
    expect(styles.color).toBeTruthy();
    expect(styles.backgroundColor).toBeTruthy();
  });
});

API Integration Testing

// e2e/​api-integration.spec.ts
import { test, expect } from '@playwright/​test';
import { TestHelpers } from './​utils/​test-helpers';

test.describe('API Integration', () => {
  test('should load data from API', async ({ page }) => {
    await page.goto('/​products');
    
    // Wait for API response
    const response = await TestHelpers.waitForAPIResponse(page, '/​api/​products');
    expect(response.status()).toBe(200);
    
    // Verify data is displayed
    await expect(page.locator('[data-testid=product-grid]')).toBeVisible();
    const productCards = page.locator('[data-testid=product-card]');
    await expect(productCards.first()).toBeVisible();
  });

  test('should handle API errors gracefully', async ({ page }) => {
    // Mock API error
    await page.route('/​api/​products', route => {
      route.fulfill({
        status: 500,
        contentType: 'application/​json',
        body: JSON.stringify({ error: 'Internal server error' })
      });
    });
    
    await page.goto('/​products');
    
    // Should show error state
    await expect(page.locator('[data-testid=error-message]')).toBeVisible();
    await expect(page.locator('[data-testid=error-message]')).toContainText('Failed to load products');
  });

  test('should handle form submissions', async ({ page }) => {
    const testData = await TestHelpers.generateTestData();
    
    await page.goto('/​register');
    
    // Fill registration form
    await page.fill('[data-testid=name-input]', testData.user.name);
    await page.fill('[data-testid=email-input]', testData.user.email);
    await page.fill('[data-testid=password-input]', testData.user.password);
    
    // Mock API response
    await page.route('/​api/​auth/​register', route => {
      route.fulfill({
        status: 201,
        contentType: 'application/​json',
        body: JSON.stringify({ 
          success: true, 
          user: { id: 1, email: testData.user.email }
        })
      });
    });
    
    await page.click('[data-testid=register-button]');
    
    // Verify successful registration
    await expect(page).toHaveURL('/​dashboard');
    await expect(page.locator('[data-testid=success-message]')).toBeVisible();
  });
});

Performance Testing

// e2e/​performance.spec.ts
import { test, expect } from '@playwright/​test';

test.describe('Performance', () => {
  test('should load within performance budget', async ({ page }) => {
    const startTime = Date.now();
    
    await page.goto('/');
    
    // Wait for page to be fully loaded
    await page.waitForLoadState('networkidle');
    
    const loadTime = Date.now() - startTime;
    
    // Performance budget: 3 seconds
    expect(loadTime).toBeLessThan(3000);
  });

  test('should have good Core Web Vitals', async ({ page }) => {
    const metrics = await page.evaluate(() => {
      return new Promise((resolve) => {
        // @ts-ignore
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          resolve({
            lcp: entries.find(entry => entry.name === 'largest-contentful-paint')?.startTime || 0,
            fid: entries.find(entry => entry.name === 'first-input')?.processingStart - entries.find(entry => entry.name === 'first-input')?.startTime || 0,
            cls: entries.find(entry => entry.name === 'cumulative-layout-shift')?.value || 0,
          });
        }).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'cumulative-layout-shift'] });
      });
    });

    // Core Web Vitals thresholds
    expect(metrics.lcp).toBeLessThan(2500); // LCP: 2.5s
    expect(metrics.fid).toBeLessThan(100);   // FID: 100ms
    expect(metrics.cls).toBeLessThan(0.1);   // CLS: 0.1
  });
});

Test Data Management

// e2e/​fixtures/​test-data.ts
import { test as base } from '@playwright/​test';

type TestFixtures = {
  testData: {
    user: { email: string; name: string; password: string };
    product: { name: string; price: number; description: string };
  };
};

export const test = base.extend<TestFixtures>({
  testData: async ({}, use) => {
    const data = {
      user: {
        email: `test-${Date.now()}@example.com`,
        name: 'Test User',
        password: 'TestPassword123!',
      },
      product: {
        name: 'Test Product',
        price: 29.99,
        description: 'A test product for E2E testing',
      }
    };
    
    await use(data);
  },
});

export { expect };

Best Practices

  1. Page Object Model - Separate page interactions from test logic
  2. Test data isolation - Use unique test data to avoid conflicts
  3. Parallel execution - Structure tests to run in parallel
  4. Retry logic - Use retries for flaky network-dependent tests
  5. Accessibility testing - Include accessibility checks in E2E tests

Stop Conditions

STOP and escalate if:

  • Testing strategy unclear or incomplete
  • Test environment not properly configured
  • Browser/​device coverage not defined
  • CI/​CD integration not established

Skill Version: 1.0.0