npx claudepluginhub joel611/claude-plugins --plugin playwrightWant just this skill?
Add to a custom plugin, then install with one command.
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.
This skill uses the workspace's default tool permissions.
resources/fixtures.tsresources/playwright.config.tsresources/test-template.tsresources/utils.tsWhen to Use This Skill
Use this skill when you need to:
- Create new E2E tests from user stories or requirements
- Generate test files for new features or pages
- Convert manual test cases into automated tests
- Scaffold a complete test suite for a new application
- Create tests with proper fixtures and configuration
Do NOT use this skill when:
- You need to debug existing tests (use test-debugger skill)
- You want to refactor or maintain existing tests (use test-maintainer skill)
- You need to create Page Object Models (use page-object-builder skill)
Prerequisites
Before using this skill:
- Playwright should be installed in the project (
npm install -D @playwright/test) - Basic understanding of the application under test (URLs, main flows)
- Knowledge of what functionality needs to be tested
- Access to the application's UI or design documentation
Instructions
Step 1: Gather Test Requirements
Ask the user for:
- Feature/functionality to test
- User flow or scenario description
- Expected outcomes (what should happen)
- Test data requirements (if any)
- Page URL(s) involved in the test
- Data-testid values (or offer to suggest them based on element purpose)
Step 2: Analyze and Plan
Review the requirements and:
- Break down the user flow into discrete steps
- Identify all page elements that need interaction
- Determine what assertions are needed
- Plan the test structure (setup, actions, verifications)
- Identify any fixtures or utilities needed
Step 3: Generate Test File
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:
- Descriptive test names (what behavior is tested)
- Proper async/await usage
- data-testid locators ONLY
- Explicit waits (waitForSelector, waitForLoadState)
- Clear assertions with expect()
- Comments for AAA sections
- TypeScript types
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
Step 4: Add Configuration (if needed)
If this is the first test, generate playwright.config.ts:
- Base URL configuration
- Timeout settings (30s default)
- Retry logic (2 retries for flaky tests)
- Screenshot on failure
- Trace on first retry
- Parallel execution settings
Step 5: Include Fixtures (if needed)
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)
},
});
Step 6: Validate Generated Test
Ensure the test includes:
- Descriptive test name
- Proper test.describe grouping
- Only data-testid locators
- No hardcoded waits (no page.waitForTimeout())
- Explicit waits for elements
- Clear AAA structure
- TypeScript types
- Meaningful assertions
- Comments for complex logic
Step 7: Provide Usage Instructions
Tell the user:
- Where the test file was created
- How to run the test:
npx playwright test <filename> - How to run in debug mode:
npx playwright test --debug <filename> - How to view traces:
npx playwright show-trace trace.zip - What data-testid values need to be added to the UI (if suggesting new ones)
Examples
Example 1: Simple Login Test
Input: "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 element
Example 2: E-commerce Product Purchase
Input: "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+/,
);
});
});
Example 3: Form Validation Test
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();
});
});
Best Practices
Test Structure
- One scenario per test: Each test should verify one specific behavior
- Descriptive names: Use "should [expected behavior]" format
- AAA pattern: Always follow Arrange-Act-Assert structure
- Independent tests: Tests should not depend on each other
- Clean state: Each test should start with a clean state (use fixtures)
Locators
- data-testid ONLY: Never use CSS selectors, XPath, or text-based locators
- Semantic naming: Use descriptive testid names (e.g., "submit-button" not "btn1")
- Stable locators: data-testid values should not change with UI updates
- Unique identifiers: Each testid should be unique on the page
Async/Await
- Always await: Every Playwright action should be awaited
- No hardcoded waits: Use
waitForSelector,waitForLoadState, notwaitForTimeout - Wait for elements: Explicitly wait for elements before interaction
- Wait for navigation: Use
waitForURLafter actions that navigate
Assertions
- Explicit expectations: Use
expect()with specific matchers - Wait for conditions: Assertions automatically wait (default 5s)
- Multiple assertions: It's OK to have multiple assertions per test
- Negative assertions: Use
.not.toBeVisible()for negative cases
Error Handling
- Screenshot on failure: Configure in playwright.config.ts
- Trace on retry: Enable trace recording for debugging
- Meaningful errors: Assertions should provide clear error messages
- Timeout configuration: Set appropriate timeouts (30s default)
Common Issues and Solutions
Issue 1: Test Times Out
Problem: Test fails with "Timeout 30000ms exceeded" error
Solutions:
- Add explicit waits before interactions:
await page.waitForSelector('[data-testid="element"]') - Increase timeout for slow operations:
{ timeout: 60000 } - Wait for network to be idle:
await page.waitForLoadState('networkidle') - Check if element is actually present in the page
- Verify the data-testid value is correct
Issue 2: Element Not Found
Problem: "Element not found" or "locator.click: Target closed" errors
Solutions:
- Verify the data-testid value matches the HTML attribute
- Add wait before interaction:
await expect(locator).toBeVisible() - Check if element is in a frame/iframe (requires frame handling)
- Ensure page has loaded:
await page.waitForLoadState('domcontentloaded') - Verify element isn't dynamically loaded (wait for it explicitly)
Issue 3: Flaky Tests
Problem: Test passes sometimes but fails randomly
Solutions:
- Remove all
page.waitForTimeout()calls (use explicit waits instead) - Wait for specific conditions, not arbitrary time periods
- Use
waitForLoadState('networkidle')for AJAX-heavy pages - Enable retries in config (2 retries recommended)
- Check for race conditions (multiple elements with same testid)
- Ensure test isolation (clean state between tests)
Issue 4: Wrong Locator Strategy
Problem: Generated test uses CSS selectors or XPath
Solutions:
- ALWAYS use
page.locator('[data-testid="element-name"]')format - Never use
page.locator('.class-name')orpage.locator('#id') - Never use
page.getByRole(),page.getByText(), orpage.getByLabel() - If data-testid doesn't exist, suggest adding it to the UI code
- Document all required data-testid values for developers
Issue 5: Test Doesn't Match Requirements
Problem: Generated test doesn't fully cover the specified scenario
Solutions:
- Re-read the requirements carefully
- Break down complex flows into smaller steps
- Verify all user actions are included
- Ensure all expected outcomes have assertions
- Ask user for clarification if requirements are ambiguous
- Add comments explaining each step of the test
Resources
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 operations