This skill should be used when writing Playwright tests, creating test suites, debugging flaky tests, choosing selectors, mocking API responses, setting up test fixtures, or following Playwright best practices. Triggers on "playwright test", "e2e test", "end-to-end test", "spec file", "page object", "test fixture", "flaky test", "playwright selector", "playwright mock", "playwright assertion".
From playwrightnpx claudepluginhub trusted-american/marketplace --plugin playwrightThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Always prefer accessible selectors. In order of preference:
page.getByRole('button', { name: 'Submit' }) — best, mirrors user intentpage.getByLabel('Email address') — great for form inputspage.getByText('Welcome back') — good for static textpage.getByPlaceholder('Enter email') — acceptable for inputspage.getByTestId('submit-btn') — fallback when no semantic option existsNever use:
page.locator('.css-class') — breaks on styling changespage.locator('div > span:nth-child(2)') — extremely fragilepage.locator('#generated-id') — breaks on framework changesNever use waitForTimeout. Always use condition-based waits:
// Wait for element
await expect(page.getByRole('heading')).toBeVisible();
// Wait for navigation
await page.waitForURL('**/dashboard');
// Wait for API response
const responsePromise = page.waitForResponse('**/api/data');
await page.getByRole('button', { name: 'Load' }).click();
await responsePromise;
// Wait for specific outcome after action (avoid networkidle — unreliable in SPAs)
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Submitted successfully')).toBeVisible();
Use page.route() for API mocking:
// Mock success response
await page.route('**/api/users', route =>
route.fulfill({ json: [{ id: 1, name: 'Test User' }] })
);
// Mock error response
await page.route('**/api/users', route =>
route.fulfill({ status: 500, json: { error: 'Server error' } })
);
// Mock network failure
await page.route('**/api/users', route => route.abort());
// Mock slow response
await page.route('**/api/users', async route => {
await new Promise(resolve => setTimeout(resolve, 3000));
await route.fulfill({ json: [] });
});
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/feature-page');
});
test('should display initial state correctly', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Feature' })).toBeVisible();
});
test('should handle user interaction', async ({ page }) => {
await page.getByRole('button', { name: 'Action' }).click();
await expect(page.getByText('Result')).toBeVisible();
});
});
page via the fixture — never share page statetest.beforeEach for common navigation, not for shared data setuppage.route()test.describe.serial unless tests truly cannot be parallelizedtest.afterEach// URL assertions
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle('Dashboard');
// Element visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).not.toBeVisible();
// Element state
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toHaveAttribute('aria-expanded', 'true');
// Text content
await expect(locator).toHaveText('exact text');
await expect(locator).toContainText('partial text');
// Count
await expect(locator).toHaveCount(5);
// Input values
await expect(locator).toHaveValue('input value');
test('should show validation errors for empty required fields', async ({ page }) => {
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Email is required')).toBeVisible();
await expect(page.getByText('Password is required')).toBeVisible();
});
test('should show loading indicator during data fetch', async ({ page }) => {
await page.route('**/api/data', async route => {
await new Promise(r => setTimeout(r, 100));
await route.fulfill({ json: { items: [] } });
});
await page.goto('/page');
await expect(page.getByRole('progressbar')).toBeVisible();
await expect(page.getByRole('progressbar')).toBeHidden();
});
test('should display error message on API failure', async ({ page }) => {
await page.route('**/api/data', route =>
route.fulfill({ status: 500, json: { error: 'Internal Server Error' } })
);
await page.goto('/page');
await expect(page.getByRole('alert')).toContainText('Something went wrong');
});
test('should redirect to login when not authenticated', async ({ page }) => {
await page.goto('/protected-page');
await expect(page).toHaveURL(/.*login/);
});