Claude Code Plugins

Community-maintained marketplace

Feedback

Core Playwright concepts, setup, and test execution for E2E 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 playwright-basics
description Core Playwright concepts, setup, and test execution for E2E testing

Playwright Basics

Playwright is a Node.js library for end-to-end testing of web applications. This guide covers the essentials for testing the Clientt CRM Phoenix application.

Project Structure

The Playwright tests are located in the playwright_tests/ directory at the project root, independent of the Phoenix application:

playwright_tests/
├── tests/
│   ├── features/            # Feature-based tests (BDD scenarios)
│   ├── examples/            # Example test patterns
│   ├── support/             # Helper functions and page objects
│   └── fixtures/            # Custom test fixtures
├── playwright.config.js     # Configuration
├── package.json            # Dependencies
└── README.md               # Documentation

Running Tests

All commands should be run from the playwright_tests directory:

# Run all tests
npm test

# Run with browser UI visible
npm run test:headed

# Interactive UI mode
npm run test:ui

# Debug mode
npm run test:debug

# Run specific test file
npx playwright test tests/features/authentication.spec.js

# Run specific browser
npx playwright test --project=chromium

Basic Test Structure

// @ts-check
const { test, expect } = require('@playwright/test');

test.describe('Feature Name', () => {

  test('should do something', async ({ page }) => {
    // Navigate to page
    await page.goto('/some-path');

    // Interact with elements
    await page.fill('input[name="email"]', 'test@example.com');
    await page.click('button[type="submit"]');

    // Wait for navigation/network
    await page.waitForLoadState('networkidle');

    // Make assertions
    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator('h1')).toContainText('Welcome');
  });
});

Common Playwright Operations

Navigation

// Navigate to URL
await page.goto('/sign-in');

// Navigate with options
await page.goto('/dashboard', { waitUntil: 'networkidle' });

// Go back/forward
await page.goBack();
await page.goForward();

// Reload
await page.reload();

Finding Elements

// CSS selectors
await page.locator('.class-name')
await page.locator('#id')
await page.locator('button[type="submit"]')

// Text content
await page.locator('text=Sign In')
await page.locator('text=/log.*out/i')  // Regex

// Data attributes (preferred)
await page.locator('[data-test="submit-button"]')

// Get multiple elements
const items = await page.locator('.list-item').all();

Interactions

// Click
await page.click('button');

// Fill input
await page.fill('input[name="email"]', 'test@example.com');

// Type (with delay between keystrokes)
await page.type('input', 'Hello', { delay: 100 });

// Check/uncheck
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');

// Select dropdown
await page.selectOption('select#country', 'USA');

// Upload file
await page.setInputFiles('input[type="file"]', 'path/to/file');

// Keyboard
await page.keyboard.press('Enter');
await page.keyboard.type('Hello World');

Waiting

// Wait for load state
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');

// Wait for selector
await page.waitForSelector('.result');
await page.waitForSelector('.loading', { state: 'hidden' });

// Wait for URL
await page.waitForURL('**/dashboard');
await page.waitForURL(/.*success/);

// Wait for timeout (avoid if possible)
await page.waitForTimeout(1000);

Assertions

const { expect } = require('@playwright/test');

// Page assertions
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).toHaveTitle(/CRM/);

// Element assertions
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('.error')).toBeHidden();
await expect(page.locator('h1')).toContainText('Welcome');
await expect(page.locator('input')).toHaveValue('test@example.com');
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();

// Count
await expect(page.locator('.item')).toHaveCount(5);

Configuration

The playwright.config.js file contains:

  • baseURL: Default URL for the application (http://localhost:4002)
  • timeout: Maximum test execution time (30 seconds)
  • projects: Browser configurations (chromium, firefox, webkit)
  • webServer: Automatically starts Phoenix server before tests
  • use: Shared settings (screenshots, videos, traces)

Environment Variables

Create a .env file in playwright_tests/:

BASE_URL=http://localhost:4002
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=SecurePassword123!

Access in tests:

const email = process.env.TEST_USER_EMAIL;

Test Hooks

test.describe('Feature', () => {

  // Runs once before all tests
  test.beforeAll(async () => {
    // Setup code
  });

  // Runs before each test
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  // Runs after each test
  test.afterEach(async ({ page }) => {
    // Cleanup
  });

  // Runs once after all tests
  test.afterAll(async () => {
    // Teardown code
  });

  test('my test', async ({ page }) => {
    // Test code
  });
});

Debugging

Debug Mode

npm run test:debug

This opens Playwright Inspector for stepping through tests.

Console Logs

// Listen to console messages
page.on('console', msg => console.log('PAGE LOG:', msg.text()));

// Listen to page errors
page.on('pageerror', error => console.log('PAGE ERROR:', error));

Screenshots

// Take screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });

Pause Execution

// Pause in headed mode
await page.pause();

Best Practices

  1. Use baseURL: Configure in playwright.config.js and use relative paths
  2. Wait for network idle: After navigation and form submissions
  3. Use data-test attributes: More stable than CSS classes
  4. Explicit waits: Use waitForSelector instead of waitForTimeout
  5. Isolate tests: Each test should be independent
  6. Clean up: Remove test data after tests (or use transactions)
  7. Use fixtures: For common setup like authentication

Common Patterns

Form Submission

await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');

Authentication Check

// Check if logged in
const isLoggedIn = !page.url().includes('sign-in');

Multiple Windows/Tabs

const [newPage] = await Promise.all([
  context.waitForEvent('page'),
  page.click('a[target="_blank"]')
]);

await newPage.waitForLoadState();

Troubleshooting

Test Timeouts

  • Increase timeout in config: timeout: 60 * 1000
  • Add explicit waits: await page.waitForLoadState('networkidle')

Element Not Found

  • Use Playwright Inspector: npm run test:debug
  • Check selector in browser DevTools
  • Add waits: await page.waitForSelector('.element')

Flaky Tests

  • Use proper waits (networkidle, selector visible)
  • Avoid fixed timeouts
  • Check for race conditions
  • Ensure test isolation

Resources