playwright-e2e-testing

Cross-browser end-to-end testing with auto-wait, parallel execution, and built-in debugging tools. Supports Chromium, Firefox, and WebKit across desktop and mobile devices with automatic element waiting and network control Includes role-based locators, page object model patterns, and storage-state authentication for maintainable test suites Offers visual regression testing via screenshot comparison, video recording, and trace viewer for debugging failed tests Built-in test runner with parallel execution, retries, multiple reporters, and CI/CD integration templates for GitHub Actions and Docker

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill playwright-e2e-testing
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Playwright E2E Testing Skill

progressive_disclosure: entry_point: summary: "Modern E2E testing framework with cross-browser automation and built-in test runner" when_to_use: - "When testing web applications end-to-end" - "When needing cross-browser testing" - "When testing user flows and interactions" - "When needing screenshot/video recording" quick_start: - "npm init playwright@latest" - "Choose TypeScript and test location" - "npx playwright test" - "npx playwright show-report" token_estimate: entry: 75-90 full: 4200-5200

Overview

Playwright is a modern end-to-end testing framework that provides cross-browser automation with a built-in test runner, auto-wait mechanisms, and excellent developer experience.

Key Features

  • Auto-wait: Automatically waits for elements to be ready
  • Cross-browser: Chromium, Firefox, WebKit support
  • Built-in runner: Parallel execution, retries, reporters
  • Network control: Mock and intercept network requests
  • Debugging: UI mode, trace viewer, inspector

Installation

# Initialize new Playwright project

npm init playwright@latest

# Or add to existing project

npm install -D @playwright/test

# Install browsers

npx playwright install

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',

  use: {

    baseURL: 'http://localhost:3000',

    trace: 'on-first-retry',

    screenshot: 'only-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'] },

    },

  ],

  webServer: {

    command: 'npm run start',

    url: 'http://localhost:3000',

    reuseExistingServer: !process.env.CI,

  },

});

Fundamentals

Basic Test Structure

import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {

  await page.goto('https://example.com');

  // Wait for element and check visibility

  const title = page.locator('h1');

  await expect(title).toBeVisible();

  await expect(title).toHaveText('Example Domain');

  // Get page title

  await expect(page).toHaveTitle(/Example/);

});

test.describe('User authentication', () => {

  test('should login successfully', async ({ page }) => {

    await page.goto('/login');

    await page.fill('[name="username"]', 'testuser');

    await page.fill('[name="password"]', 'password123');

    await page.click('button[type="submit"]');

    await expect(page).toHaveURL('/dashboard');

    await expect(page.locator('.welcome-message')).toContainText('Welcome');

  });

  test('should show error for invalid credentials', async ({ page }) => {

    await page.goto('/login');

    await page.fill('[name="username"]', 'invalid');

    await page.fill('[name="password"]', 'wrong');

    await page.click('button[type="submit"]');

    await expect(page.locator('.error-message')).toBeVisible();

    await expect(page.locator('.error-message')).toHaveText('Invalid credentials');

  });

});

Test Hooks

import { test, expect } from '@playwright/test';

test.describe('Dashboard tests', () => {

  test.beforeEach(async ({ page }) => {

    // Run before each test

    await page.goto('/dashboard');

    await page.waitForLoadState('networkidle');

  });

  test.afterEach(async ({ page }) => {

    // Cleanup after each test

    await page.close();

  });

  test.beforeAll(async ({ browser }) => {

    // Run once before all tests in describe block

    console.log('Starting test suite');

  });

  test.afterAll(async ({ browser }) => {

    // Run once after all tests

    console.log('Test suite complete');

  });

  test('displays user data', async ({ page }) => {

    await expect(page.locator('.user-name')).toBeVisible();

  });

});

Locator Strategies

Best Practice: Role-based Locators

import { test, expect } from '@playwright/test';

test('accessible locators', async ({ page }) => {

  await page.goto('/form');

  // By role (BEST - accessible and stable)

  await page.getByRole('button', { name: 'Submit' }).click();

  await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');

  await page.getByRole('checkbox', { name: 'Subscribe' }).check();

  await page.getByRole('link', { name: 'Learn more' }).click();

  // By label (good for forms)

  await page.getByLabel('Password').fill('secret123');

  // By placeholder

  await page.getByPlaceholder('Search...').fill('query');

  // By text

  await page.getByText('Welcome back').click();

  await page.getByText(/hello/i).isVisible();

  // By test ID (good for dynamic content)

  await page.getByTestId('user-profile').click();

  // By title

  await page.getByTitle('Close dialog').click();

  // By alt text (images)

  await page.getByAltText('User avatar').click();

});

CSS and XPath Locators

test('CSS and XPath locators', async ({ page }) => {

  // CSS selectors

  await page.locator('button.primary').click();

  await page.locator('#user-menu').click();

  await page.locator('[data-testid="submit-btn"]').click();

  await page.locator('div.card:first-child').click();

  // XPath (use sparingly)

  await page.locator('xpath=//button[contains(text(), "Submit")]').click();

  // Chaining locators

  const form = page.locator('form#login-form');

  await form.locator('input[name="email"]').fill('user@example.com');

  await form.locator('button[type="submit"]').click();

  // Filter locators

  await page.getByRole('listitem')

    .filter({ hasText: 'Product 1' })

    .getByRole('button', { name: 'Add to cart' })

    .click();

});

Page Object Model

Page Class Pattern

// pages/LoginPage.ts

import { Page, Locator } from '@playwright/test';

export class LoginPage {

  readonly page: Page;

  readonly usernameInput: Locator;

  readonly passwordInput: Locator;

  readonly submitButton: Locator;

  readonly errorMessage: Locator;

  constructor(page: Page) {

    this.page = page;

    this.usernameInput = page.getByLabel('Username');

    this.passwordInput = page.getByLabel('Password');

    this.submitButton = page.getByRole('button', { name: 'Log in' });

    this.errorMessage = page.locator('.error-message');

  }

  async goto() {

    await this.page.goto('/login');

  }

  async login(username: string, password: string) {

    await this.usernameInput.fill(username);

    await this.passwordInput.fill(password);

    await this.submitButton.click();

  }

  async expectErrorMessage(message: string) {

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

    await expect(this.errorMessage).toHaveText(message);

  }

}

// pages/DashboardPage.ts

export class DashboardPage {

  readonly page: Page;

  readonly welcomeMessage: Locator;

  readonly logoutButton: Locator;

  constructor(page: Page) {

    this.page = page;

    this.welcomeMessage = page.locator('.welcome-message');

    this.logoutButton = page.getByRole('button', { name: 'Logout' });

  }

  async waitForLoad() {

    await this.welcomeMessage.waitFor({ state: 'visible' });

  }

  async logout() {

    await this.logoutButton.click();

  }

}

// tests/auth.spec.ts

import { test, expect } from '@playwright/test';

import { LoginPage } from '../pages/LoginPage';

import { DashboardPage } from '../pages/DashboardPage';

test('successful login flow', async ({ page }) => {

  const loginPage = new LoginPage(page);

  const dashboard = new DashboardPage(page);

  await loginPage.goto();

  await loginPage.login('testuser', 'password123');

  await dashboard.waitForLoad();

  await expect(dashboard.welcomeMessage).toContainText('Welcome');

});

Component Pattern

// components/NavigationComponent.ts

import { Page, Locator } from '@playwright/test';

export class NavigationComponent {

  readonly page: Page;

  readonly homeLink: Locator;

  readonly profileLink: Locator;

  readonly searchInput: Locator;

  constructor(page: Page) {

    this.page = page;

    const nav = page.locator('nav');

    this.homeLink = nav.getByRole('link', { name: 'Home' });

    this.profileLink = nav.getByRole('link', { name: 'Profile' });

    this.searchInput = nav.getByPlaceholder('Search...');

  }

  async navigateToProfile() {

    await this.profileLink.click();

  }

  async search(query: string) {

    await this.searchInput.fill(query);

    await this.searchInput.press('Enter');

  }

}

User Interactions

Form Interactions

test('form interactions', async ({ page }) => {

  await page.goto('/form');

  // Text inputs

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

  await page.type('textarea[name="message"]', 'Hello', { delay: 100 });

  // Checkboxes

  await page.check('input[type="checkbox"][name="subscribe"]');

  await page.uncheck('input[type="checkbox"][name="spam"]');

  // Radio buttons

  await page.check('input[type="radio"][value="option1"]');

  // Select dropdowns

  await page.selectOption('select[name="country"]', 'US');

  await page.selectOption('select[name="color"]', { label: 'Blue' });

  await page.selectOption('select[name="size"]', { value: 'large' });

  // Multi-select

  await page.selectOption('select[multiple]', ['value1', 'value2']);

  // File uploads

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

  await page.setInputFiles('input[type="file"]', [

    'file1.jpg',

    'file2.jpg'

  ]);

  // Clear file input

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

});

Mouse and Keyboard

test('mouse and keyboard interactions', async ({ page }) => {

  // Click variations

  await page.click('button');

  await page.dblclick('button'); // Double click

  await page.click('button', { button: 'right' }); // Right click

  await page.click('button', { modifiers: ['Shift'] }); // Shift+click

  // Hover

  await page.hover('.tooltip-trigger');

  await expect(page.locator('.tooltip')).toBeVisible();

  // Drag and drop

  await page.dragAndDrop('#draggable', '#droppable');

  // Keyboard

  await page.keyboard.press('Enter');

  await page.keyboard.press('Control+A');

  await page.keyboard.type('Hello World');

  await page.keyboard.down('Shift');

  await page.keyboard.press('ArrowDown');

  await page.keyboard.up('Shift');

  // Focus

  await page.focus('input[name="email"]');

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

});

Waiting Strategies

test('waiting strategies', async ({ page }) => {

  // Wait for element

  await page.waitForSelector('.dynamic-content');

  await page.waitForSelector('.modal', { state: 'visible' });

  await page.waitForSelector('.loading', { state: 'hidden' });

  // Wait for load state

  await page.waitForLoadState('load');

  await page.waitForLoadState('domcontentloaded');

  await page.waitForLoadState('networkidle');

  // Wait for URL

  await page.waitForURL('**/dashboard');

  await page.waitForURL(/\/product\/\d+/);

  // Wait for function

  await page.waitForFunction(() => {

    return document.querySelectorAll('.item').length > 5;

  });

  // Wait for timeout (avoid if possible)

  await page.waitForTimeout(1000);

  // Wait for event

  await page.waitForEvent('load');

  await page.waitForEvent('popup');

});

Assertions

Common Assertions

import { test, expect } from '@playwright/test';

test('assertions', async ({ page }) => {

  await page.goto('/dashboard');

  // Visibility

  await expect(page.locator('.header')).toBeVisible();

  await expect(page.locator('.loading')).toBeHidden();

  await expect(page.locator('.optional')).not.toBeVisible();

  // Text content

  await expect(page.locator('h1')).toHaveText('Dashboard');

  await expect(page.locator('h1')).toContainText('Dash');

  await expect(page.locator('.message')).toHaveText(/welcome/i);

  // Attributes

  await expect(page.locator('button')).toBeEnabled();

  await expect(page.locator('button')).toBeDisabled();

  await expect(page.locator('input')).toHaveAttribute('type', 'email');

  await expect(page.locator('input')).toHaveValue('test@example.com');

  // CSS

  await expect(page.locator('.button')).toHaveClass('btn-primary');

  await expect(page.locator('.button')).toHaveClass(/btn-/);

  await expect(page.locator('.element')).toHaveCSS('color', 'rgb(255, 0, 0)');

  // Count

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

  // URL and title

  await expect(page).toHaveURL('http://localhost:3000/dashboard');

  await expect(page).toHaveURL(/dashboard$/);

  await expect(page).toHaveTitle('Dashboard - My App');

  await expect(page).toHaveTitle(/Dashboard/);

  // Screenshot comparison

  await expect(page).toHaveScreenshot('dashboard.png');

  await expect(page.locator('.widget')).toHaveScreenshot('widget.png');

});

Custom Assertions

test('custom matchers', async ({ page }) => {

  // Soft assertions (continue test on failure)

  await expect.soft(page.locator('.title')).toHaveText('Welcome');

  await expect.soft(page.locator('.subtitle')).toBeVisible();

  // Multiple elements

  const items = page.locator('.item');

  await expect(items).toHaveCount(3);

  await expect(items.nth(0)).toContainText('First');

  await expect(items.nth(1)).toContainText('Second');

  // Poll assertions

  await expect(async () => {

    const response = await page.request.get('/api/status');

    expect(response.ok()).toBeTruthy();

  }).toPass({

    timeout: 10000,

    intervals: [1000, 2000, 5000],

  });

});

Authentication Patterns

Storage State Pattern

// auth.setup.ts - Run once to save auth state

import { test as setup } from '@playwright/test';

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

setup('authenticate', async ({ page }) => {

  await page.goto('/login');

  await page.fill('[name="username"]', 'testuser');

  await page.fill('[name="password"]', 'password123');

  await page.click('button[type="submit"]');

  await page.waitForURL('/dashboard');

  // Save authentication state

  await page.context().storageState({ path: authFile });

});

// playwright.config.ts

export default defineConfig({

  projects: [

    {

      name: 'setup',

      testMatch: /.*\.setup\.ts/,

    },

    {

      name: 'chromium',

      use: {

        ...devices['Desktop Chrome'],

        storageState: authFile,

      },

      dependencies: ['setup'],

    },

  ],

});

// tests/dashboard.spec.ts - Already authenticated

test('view dashboard', async ({ page }) => {

  await page.goto('/dashboard');

  // Already logged in!

  await expect(page.locator('.user-menu')).toBeVisible();

});

Multiple User Roles

// fixtures/auth.ts

import { test as base } from '@playwright/test';

type Fixtures = {

  adminPage: Page;

  userPage: Page;

};

export const test = base.extend<Fixtures>({

  adminPage: async ({ browser }, use) => {

    const context = await browser.newContext({

      storageState: 'playwright/.auth/admin.json',

    });

    const page = await context.newPage();

    await use(page);

    await context.close();

  },

  userPage: async ({ browser }, use) => {

    const context = await browser.newContext({

      storageState: 'playwright/.auth/user.json',

    });

    const page = await context.newPage();

    await use(page);

    await context.close();

  },

});

// tests/permissions.spec.ts

import { test } from '../fixtures/auth';

test('admin can access admin panel', async ({ adminPage }) => {

  await adminPage.goto('/admin');

  await expect(adminPage.locator('.admin-panel')).toBeVisible();

});

test('regular user cannot access admin panel', async ({ userPage }) => {

  await userPage.goto('/admin');

  await expect(userPage.locator('.access-denied')).toBeVisible();

});

Network Control

Request Mocking

test('mock API responses', async ({ page }) => {

  // Mock API response

  await page.route('**/api/users', route => {

    route.fulfill({

      status: 200,

      contentType: 'application/json',

      body: JSON.stringify({

        users: [

          { id: 1, name: 'John Doe' },

          { id: 2, name: 'Jane Smith' },

        ],

      }),

    });

  });

  await page.goto('/users');

  await expect(page.locator('.user-list')).toContainText('John Doe');

});

test('mock with conditions', async ({ page }) => {

  await page.route('**/api/**', route => {

    const url = route.request().url();

    if (url.includes('/users/1')) {

      route.fulfill({

        status: 200,

        body: JSON.stringify({ id: 1, name: 'Test User' }),

      });

    } else if (url.includes('/users')) {

      route.fulfill({

        status: 200,

        body: JSON.stringify({ users: [] }),

      });

    } else {

      route.continue();

    }

  });

});

test('simulate network errors', async ({ page }) => {

  await page.route('**/api/data', route => {

    route.abort('failed');

  });

  await page.goto('/data');

  await expect(page.locator('.error-message')).toBeVisible();

});

Request Interception

test('intercept and modify requests', async ({ page }) => {

  // Modify request headers

  await page.route('**/api/**', route => {

    const headers = route.request().headers();

    route.continue({

      headers: {

        ...headers,

        'X-Custom-Header': 'test-value',

      },

    });

  });

  // Modify POST data

  await page.route('**/api/submit', route => {

    const postData = route.request().postDataJSON();

    route.continue({

      postData: JSON.stringify({

        ...postData,

        timestamp: Date.now(),

      }),

    });

  });

});

test('wait for API response', async ({ page }) => {

  // Wait for specific request

  const responsePromise = page.waitForResponse('**/api/users');

  await page.click('button#load-users');

  const response = await responsePromise;

  expect(response.status()).toBe(200);

  const data = await response.json();

  expect(data.users).toHaveLength(10);

});

Test Organization

Custom Fixtures

// fixtures/todos.ts

import { test as base } from '@playwright/test';

type TodoFixtures = {

  todoPage: TodoPage;

  createTodo: (title: string) => Promise<void>;

};

export const test = base.extend<TodoFixtures>({

  todoPage: async ({ page }, use) => {

    const todoPage = new TodoPage(page);

    await todoPage.goto();

    await use(todoPage);

  },

  createTodo: async ({ page }, use) => {

    const create = async (title: string) => {

      await page.fill('.new-todo', title);

      await page.press('.new-todo', 'Enter');

    };

    await use(create);

  },

});

// tests/todos.spec.ts

import { test } from '../fixtures/todos';

test('can create new todo', async ({ todoPage, createTodo }) => {

  await createTodo('Buy groceries');

  await expect(todoPage.todoItems).toHaveCount(1);

  await expect(todoPage.todoItems).toHaveText('Buy groceries');

});

Test Tags and Filtering

test('smoke test', { tag: '@smoke' }, async ({ page }) => {

  await page.goto('/');

  await expect(page).toHaveTitle('Home');

});

test('regression test', { tag: ['@regression', '@critical'] }, async ({ page }) => {

  // Complex test

});

// Run: npx playwright test --grep @smoke

// Run: npx playwright test --grep-invert @slow

Visual Testing

Screenshot Comparison

test('visual regression', async ({ page }) => {

  await page.goto('/dashboard');

  // Full page screenshot

  await expect(page).toHaveScreenshot('dashboard.png', {

    maxDiffPixels: 100,

  });

  // Element screenshot

  await expect(page.locator('.widget')).toHaveScreenshot('widget.png');

  // Full page with scroll

  await expect(page).toHaveScreenshot('full-page.png', {

    fullPage: true,

  });

  // Mask dynamic elements

  await expect(page).toHaveScreenshot('masked.png', {

    mask: [page.locator('.timestamp'), page.locator('.avatar')],

  });

  // Custom threshold

  await expect(page).toHaveScreenshot('comparison.png', {

    maxDiffPixelRatio: 0.05, // 5% difference allowed

  });

});

Video and Trace

// playwright.config.ts

export default defineConfig({

  use: {

    video: 'retain-on-failure',

    trace: 'on-first-retry',

    screenshot: 'only-on-failure',

  },

});

// Programmatic video

test('record video', async ({ page }) => {

  await page.goto('/');

  // Test actions...

  // Video saved automatically to test-results/

});

// View trace: npx playwright show-trace trace.zip

Parallel Execution

Test Sharding

// playwright.config.ts

export default defineConfig({

  fullyParallel: true,

  workers: process.env.CI ? 4 : undefined,

});

// Run shards in CI

// npx playwright test --shard=1/4

// npx playwright test --shard=2/4

// npx playwright test --shard=3/4

// npx playwright test --shard=4/4

Serial Tests

test.describe.configure({ mode: 'serial' });

test.describe('order matters', () => {

  let orderId: string;

  test('create order', async ({ page }) => {

    // Create order

    orderId = await createOrder(page);

  });

  test('verify order', async ({ page }) => {

    // Use orderId from previous test

    await verifyOrder(page, orderId);

  });

});

CI/CD Integration

GitHub Actions

# .github/workflows/playwright.yml

name: Playwright Tests

on:

  push:

    branches: [main, master]

  pull_request:

    branches: [main, master]

jobs:

  test:

    timeout-minutes: 60

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4

        with:

          node-version: 20

      - name: Install dependencies

        run: npm ci

      - name: Install Playwright Browsers

        run: npx playwright install --with-deps

      - name: Run Playwright tests

        run: npx playwright test

      - uses: actions/upload-artifact@v4

        if: always()

        with:

          name: playwright-report

          path: playwright-report/

          retention-days: 30

Docker

FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]

Debugging

UI Mode

# Interactive debugging

npx playwright test --ui

# Debug specific test

npx playwright test --debug login.spec.ts

# Step through test

npx playwright test --headed --slow-mo=1000

Trace Viewer

// Generate trace

test('with trace', async ({ page }) => {

  await page.context().tracing.start({ screenshots: true, snapshots: true });

  // Test actions

  await page.goto('/');

  await page.context().tracing.stop({ path: 'trace.zip' });

});

// View: npx playwright show-trace trace.zip

Console Logs

test('capture console', async ({ page }) => {

  page.on('console', msg => console.log(`Browser: ${msg.text()}`));

  page.on('pageerror', error => console.error(`Error: ${error.message}`));

  await page.goto('/');

});

Best Practices

1. Use Stable Locators

// ✅ Good - Role-based, stable

await page.getByRole('button', { name: 'Submit' }).click();

await page.getByLabel('Email').fill('test@example.com');

// ❌ Bad - Fragile, implementation-dependent

await page.click('button.btn-primary.submit-btn');

await page.fill('div > form > input:nth-child(3)');

2. Leverage Auto-Waiting

// ✅ Good - Auto-waits

await page.click('button');

await expect(page.locator('.result')).toBeVisible();

// ❌ Bad - Manual waits

await page.waitForTimeout(2000);

await page.click('button');

3. Use Page Object Model

// ✅ Good - Reusable, maintainable

const loginPage = new LoginPage(page);

await loginPage.login('user', 'pass');

// ❌ Bad - Duplicated selectors

await page.fill('[name="username"]', 'user');

await page.fill('[name="password"]', 'pass');

4. Parallel-Safe Tests

// ✅ Good - Isolated

test('user signup', async ({ page }) => {

  const uniqueEmail = `user-${Date.now()}@test.com`;

  await signUp(page, uniqueEmail);

});

// ❌ Bad - Shared state

test('user signup', async ({ page }) => {

  await signUp(page, 'test@test.com'); // Conflicts in parallel

});

5. Handle Flakiness

// ✅ Good - Wait for network idle

await page.goto('/', { waitUntil: 'networkidle' });

await expect(page.locator('.data')).toBeVisible();

// Configure retries

test.describe(() => {

  test.use({ retries: 2 });

  test('flaky test', async ({ page }) => {

    // Test with auto-retry

  });

});

Common Patterns

Multi-Page Scenarios

test('popup handling', async ({ page, context }) => {

  // Listen for new page

  const popupPromise = context.waitForEvent('page');

  await page.click('a[target="_blank"]');

  const popup = await popupPromise;

  await popup.waitForLoadState();

  await expect(popup).toHaveTitle('New Window');

  await popup.close();

});

Conditional Logic

test('handle optional elements', async ({ page }) => {

  await page.goto('/');

  // Close modal if present

  const modal = page.locator('.modal');

  if (await modal.isVisible()) {

    await page.click('.modal .close-button');

  }

  // Or use count

  const cookieBanner = page.locator('.cookie-banner');

  if ((await cookieBanner.count()) > 0) {

    await page.click('.accept-cookies');

  }

});

Data-Driven Tests

const testCases = [

  { input: 'hello', expected: 'HELLO' },

  { input: 'World', expected: 'WORLD' },

  { input: '123', expected: '123' },

];

for (const { input, expected } of testCases) {

  test(`transforms "${input}" to "${expected}"`, async ({ page }) => {

    await page.goto('/transform');

    await page.fill('input', input);

    await page.click('button');

    await expect(page.locator('.result')).toHaveText(expected);

  });

}

Resources

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card