Automatically activated when user works with Playwright tests, mentions Playwright configuration, asks about selectors/locators/page objects, or has files matching *.spec.ts in e2e or tests directories. Provides Playwright-specific expertise for E2E and integration testing.
/plugin marketplace add C0ntr0lledCha0s/claude-code-plugin-automations/plugin install testing-expert@claude-code-plugin-automationsThis skill is limited to using the following tools:
REVIEW.mdassets/page-object.template.tsreferences/playwright-cheatsheet.mdscripts/check-playwright-setup.shYou are an expert in Playwright testing framework with deep knowledge of browser automation, selectors, page objects, and best practices for end-to-end testing.
Claude should automatically invoke this skill when:
*.spec.ts in e2e, tests, or playwright directories are encounteredUse {baseDir} to reference files in this skill directory:
{baseDir}/scripts/{baseDir}/references/{baseDir}/assets/This skill includes ready-to-use resources in {baseDir}:
import { test, expect } from '@playwright/test';
test.describe('Contact Form', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/contact');
});
test('should show success message after form submission', async ({ page }) => {
// Arrange
await page.getByLabel('Name').fill('Test User');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Message').fill('Hello, this is a test message.');
// Act
await page.getByRole('button', { name: 'Submit' }).click();
// Assert
await expect(page.getByText('Thank you for your message')).toBeVisible();
await expect(page.getByLabel('Name')).toBeEmpty();
});
});
// Role-based (best)
page.getByRole('button', { name: 'Submit' });
page.getByRole('textbox', { name: 'Email' });
page.getByRole('heading', { level: 1 });
// Label-based
page.getByLabel('Email address');
page.getByPlaceholder('Enter your email');
// Text-based
page.getByText('Welcome');
page.getByTitle('Close');
page.getByRole('listitem')
.filter({ hasText: 'Product 1' })
.getByRole('button', { name: 'Add' });
page.getByTestId('submit-button');
// pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
private 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');
await expect(this.emailInput).toBeVisible();
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getError(): Promise<string | null> {
if (await this.errorMessage.isVisible()) {
return this.errorMessage.textContent();
}
return null;
}
}
// Usage in test
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';
test('should login successfully', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
test('should show error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@test.com', 'wrongpassword');
const error = await loginPage.getError();
expect(error).toContain('Invalid credentials');
});
// Auto-waits for element
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByText('Count: 5')).toBeVisible();
// Negative assertions
await expect(page.getByRole('dialog')).toBeHidden();
await expect(page.getByText('Error')).not.toBeVisible();
// With custom timeout
await expect(page.getByText('Loaded')).toBeVisible({ timeout: 10000 });
// fixtures.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{
authenticatedPage: Page;
}>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@test.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('/dashboard');
await use(page);
},
});
For efficient authentication without UI login each time:
// Setup: Save auth state after login (run once)
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
// Save storage state (cookies, localStorage)
await page.context().storageState({ path: '.auth/user.json' });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { storageState: '.auth/user.json' },
dependencies: ['setup'],
},
],
});
// Tests automatically have auth state
test('dashboard loads for authenticated user', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
});
Mock API responses for reliable, fast tests:
import { test, expect } from '@playwright/test';
test('should display mocked user data', async ({ page }) => {
// Mock API response
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test User', email: 'test@example.com' }
]),
});
});
await page.goto('/users');
await expect(page.getByText('Test User')).toBeVisible();
});
test('should handle API errors gracefully', async ({ page }) => {
// Mock error response
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load users')).toBeVisible();
});
test('should handle network failure', async ({ page }) => {
// Abort network request
await page.route('**/api/data', route => route.abort());
await page.goto('/data');
await expect(page.getByText('Network error')).toBeVisible();
});
test('should handle slow responses', async ({ page }) => {
// Simulate slow API
await page.route('**/api/slow', async route => {
await new Promise(resolve => setTimeout(resolve, 3000));
await route.continue();
});
await page.goto('/slow-page');
await expect(page.getByText('Loading...')).toBeVisible();
});
// Modify request/response
test('should modify request headers', async ({ page }) => {
await page.route('**/api/**', route => {
route.continue({
headers: {
...route.request().headers(),
'X-Test-Header': 'test-value',
},
});
});
});
Integrate accessibility audits with @axe-core/playwright:
// Install: npm install @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('should pass accessibility audit', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test('should pass accessibility audit for specific section', async ({ page }) => {
await page.goto('/dashboard');
const results = await new AxeBuilder({ page })
.include('#main-content')
.exclude('#third-party-widget')
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
// Check specific rules
test('should have proper color contrast', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});
// Detailed violation reporting
test('accessibility check with detailed report', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
if (results.violations.length > 0) {
console.log('Accessibility violations:');
results.violations.forEach(violation => {
console.log(`- ${violation.id}: ${violation.description}`);
violation.nodes.forEach(node => {
console.log(` Element: ${node.html}`);
console.log(` Fix: ${node.failureSummary}`);
});
});
}
expect(results.violations).toEqual([]);
});
Compare screenshots to detect visual changes:
import { test, expect } from '@playwright/test';
test('homepage visual regression', async ({ page }) => {
await page.goto('/');
// Full page screenshot comparison
await expect(page).toHaveScreenshot('homepage.png');
});
test('component visual regression', async ({ page }) => {
await page.goto('/components');
// Element-specific screenshot
const button = page.getByRole('button', { name: 'Submit' });
await expect(button).toHaveScreenshot('submit-button.png');
});
test('visual with threshold', async ({ page }) => {
await page.goto('/');
// Allow small differences
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
threshold: 0.2,
});
});
// Update snapshots: npx playwright test --update-snapshots
// 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',
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', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
npx playwright test --debug
npx playwright test --ui
// Capture trace on failure
use: {
trace: 'on-first-retry',
}
// View trace
npx playwright show-trace trace.zip
await page.screenshot({ path: 'screenshot.png', fullPage: true });
networkidle (which fails with WebSockets, long-polling, analytics):
// Bad: networkidle is unreliable
await page.waitForLoadState('networkidle');
// Good: wait for specific content
await expect(page.getByRole('main')).toBeVisible();
await expect(page.getByTestId('data-loaded')).toBeAttached();
test.describe.parallel()When testing forms:
When testing tables/lists:
.filter()The patterns in this skill require the following minimum versions:
| Feature | Minimum Version | Notes |
|---|---|---|
| getByRole with name | 1.27+ | Role-based locators with accessible name |
| toHaveScreenshot | 1.22+ | Visual regression testing |
| storageState | 1.13+ | Authentication state persistence |
| @axe-core/playwright | 4.7+ | Accessibility testing integration |
| route.fulfill | 1.0+ | Network mocking (stable) |
| test.describe.configure | 1.24+ | Parallel/serial test configuration |
Check your Playwright version:
npx playwright --version
# Update Playwright
npm install -D @playwright/test@latest
# Update browsers
npx playwright install
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.