Generate production-ready Playwright E2E tests from natural language specifications or requirements. Creates TypeScript test files following best practices including data-testid locators, proper async/await usage, test isolation, and the AAA (Arrange-Act-Assert) pattern.
Generate production-ready Playwright E2E tests from natural language requirements. Creates TypeScript test files with data-testid locators, proper async/await, and AAA pattern when you need to create new tests from specifications.
/plugin marketplace add joel611/claude-plugins/plugin install playwright-e2e@joel-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
resources/fixtures.tsresources/playwright.config.tsresources/test-template.tsresources/utils.tsGenerate production-ready Playwright E2E tests from natural language specifications or requirements. Creates TypeScript test files following best practices including data-testid locators, proper async/await usage, test isolation, and the AAA (Arrange-Act-Assert) pattern.
Use this skill when you need to:
Do NOT use this skill when:
Before using this skill:
npm install -D @playwright/test)Ask the user for:
Review the requirements and:
Create a TypeScript test file with:
File Structure:
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test('should <specific behavior>', async ({ page }) => {
// Arrange: Setup
// Act: Perform actions
// Assert: Verify results
});
});
Required Elements:
Locator Strategy (MANDATORY):
// ✅ CORRECT: Always use data-testid
await page.locator('[data-testid="submit-button"]').click();
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// ❌ WRONG: Never use CSS selectors, XPath, or text selectors
await page.locator('.submit-btn').click(); // NO
await page.locator('//button[@type="submit"]').click(); // NO
await page.getByRole('button', { name: 'Submit' }).click(); // NO
If this is the first test, generate playwright.config.ts:
For complex setups, create custom fixtures:
import { test as base } from '@playwright/test';
type MyFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
// Setup: login
await page.goto('/login');
await page.locator('[data-testid="username"]').fill('testuser');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login-button"]').click();
await page.waitForURL('/dashboard');
await use(page);
// Teardown (if needed)
},
});
Ensure the test includes:
Tell the user:
npx playwright test <filename>npx playwright test --debug <filename>npx playwright show-trace trace.zipInput: "Create a test for the login flow. User enters username and password, clicks login button, and should see the dashboard."
Output:
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('should successfully login with valid credentials', async ({ page }) => {
// Arrange: Navigate to login page
await page.goto('/login');
await expect(page.locator('[data-testid="login-form"]')).toBeVisible();
// Act: Enter credentials and submit
await page.locator('[data-testid="username-input"]').fill('testuser@example.com');
await page.locator('[data-testid="password-input"]').fill('SecurePass123');
await page.locator('[data-testid="login-button"]').click();
// Assert: Verify successful login
await page.waitForURL('/dashboard');
await expect(page.locator('[data-testid="dashboard-header"]')).toBeVisible();
await expect(page.locator('[data-testid="user-welcome"]')).toContainText('Welcome');
});
});
Required data-testid values:
login-form - The login form containerusername-input - Username/email input fieldpassword-input - Password input fieldlogin-button - Submit buttondashboard-header - Dashboard page headeruser-welcome - Welcome message elementInput: "Test the product purchase flow: search for 'laptop', select first result, add to cart, checkout, and verify order confirmation."
Output:
import { test, expect } from '@playwright/test';
test.describe('E-commerce Purchase Flow', () => {
test('should complete product purchase from search to confirmation', async ({ page }) => {
// Arrange: Navigate to homepage
await page.goto('/');
await expect(page.locator('[data-testid="search-bar"]')).toBeVisible();
// Act: Search for product
await page.locator('[data-testid="search-input"]').fill('laptop');
await page.locator('[data-testid="search-button"]').click();
await page.waitForLoadState('networkidle');
// Act: Select first product
await expect(page.locator('[data-testid="product-card"]').first()).toBeVisible();
await page.locator('[data-testid="product-card"]').first().click();
// Act: Add to cart
await expect(page.locator('[data-testid="add-to-cart-button"]')).toBeEnabled();
await page.locator('[data-testid="add-to-cart-button"]').click();
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
// Act: Proceed to checkout
await page.locator('[data-testid="cart-icon"]').click();
await page.locator('[data-testid="checkout-button"]').click();
// Act: Fill checkout form
await page.locator('[data-testid="shipping-name"]').fill('John Doe');
await page.locator('[data-testid="shipping-address"]').fill('123 Main St');
await page.locator('[data-testid="shipping-city"]').fill('New York');
await page.locator('[data-testid="shipping-zip"]').fill('10001');
await page.locator('[data-testid="payment-card"]').fill('4242424242424242');
await page.locator('[data-testid="payment-expiry"]').fill('12/25');
await page.locator('[data-testid="payment-cvc"]').fill('123');
await page.locator('[data-testid="place-order-button"]').click();
// Assert: Verify order confirmation
await page.waitForURL(/\/order\/confirmation/);
await expect(page.locator('[data-testid="order-success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="order-number"]')).toContainText(/ORD-\d+/);
});
});
Input: "Create a test for contact form validation. Test that empty fields show error messages."
Output:
import { test, expect } from '@playwright/test';
test.describe('Contact Form Validation', () => {
test('should display validation errors for empty required fields', async ({ page }) => {
// Arrange: Navigate to contact page
await page.goto('/contact');
await expect(page.locator('[data-testid="contact-form"]')).toBeVisible();
// Act: Submit form without filling any fields
await page.locator('[data-testid="submit-button"]').click();
// Assert: Verify error messages appear
await expect(page.locator('[data-testid="name-error"]')).toBeVisible();
await expect(page.locator('[data-testid="name-error"]')).toContainText('Name is required');
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="email-error"]')).toContainText('Email is required');
await expect(page.locator('[data-testid="message-error"]')).toBeVisible();
await expect(page.locator('[data-testid="message-error"]')).toContainText('Message is required');
// Act: Fill fields correctly
await page.locator('[data-testid="name-input"]').fill('John Doe');
await page.locator('[data-testid="email-input"]').fill('john@example.com');
await page.locator('[data-testid="message-input"]').fill('Hello, this is a test message.');
// Assert: Verify errors disappear
await expect(page.locator('[data-testid="name-error"]')).not.toBeVisible();
await expect(page.locator('[data-testid="email-error"]')).not.toBeVisible();
await expect(page.locator('[data-testid="message-error"]')).not.toBeVisible();
// Act: Submit form
await page.locator('[data-testid="submit-button"]').click();
// Assert: Verify success
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});
});
waitForSelector, waitForLoadState, not waitForTimeoutwaitForURL after actions that navigateexpect() with specific matchers.not.toBeVisible() for negative casesProblem: Test fails with "Timeout 30000ms exceeded" error
Solutions:
await page.waitForSelector('[data-testid="element"]'){ timeout: 60000 }await page.waitForLoadState('networkidle')Problem: "Element not found" or "locator.click: Target closed" errors
Solutions:
await expect(locator).toBeVisible()await page.waitForLoadState('domcontentloaded')Problem: Test passes sometimes but fails randomly
Solutions:
page.waitForTimeout() calls (use explicit waits instead)waitForLoadState('networkidle') for AJAX-heavy pagesProblem: Generated test uses CSS selectors or XPath
Solutions:
page.locator('[data-testid="element-name"]') formatpage.locator('.class-name') or page.locator('#id')page.getByRole(), page.getByText(), or page.getByLabel()Problem: Generated test doesn't fully cover the specified scenario
Solutions:
The resources/ directory contains templates for common patterns:
test-template.ts - Basic test file structureplaywright.config.ts - Recommended Playwright configurationfixtures.ts - Custom fixture examples (authentication, data setup)utils.ts - Helper functions for common operationsUse when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.