378 lines
14 KiB
Markdown
378 lines
14 KiB
Markdown
# Test Generator Skill
|
|
|
|
## Purpose
|
|
|
|
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.
|
|
|
|
## When 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:
|
|
1. Playwright should be installed in the project (`npm install -D @playwright/test`)
|
|
2. Basic understanding of the application under test (URLs, main flows)
|
|
3. Knowledge of what functionality needs to be tested
|
|
4. 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:**
|
|
```typescript
|
|
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):**
|
|
```typescript
|
|
// ✅ 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:
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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 container
|
|
- `username-input` - Username/email input field
|
|
- `password-input` - Password input field
|
|
- `login-button` - Submit button
|
|
- `dashboard-header` - Dashboard page header
|
|
- `user-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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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
|
|
1. **One scenario per test**: Each test should verify one specific behavior
|
|
2. **Descriptive names**: Use "should [expected behavior]" format
|
|
3. **AAA pattern**: Always follow Arrange-Act-Assert structure
|
|
4. **Independent tests**: Tests should not depend on each other
|
|
5. **Clean state**: Each test should start with a clean state (use fixtures)
|
|
|
|
### Locators
|
|
1. **data-testid ONLY**: Never use CSS selectors, XPath, or text-based locators
|
|
2. **Semantic naming**: Use descriptive testid names (e.g., "submit-button" not "btn1")
|
|
3. **Stable locators**: data-testid values should not change with UI updates
|
|
4. **Unique identifiers**: Each testid should be unique on the page
|
|
|
|
### Async/Await
|
|
1. **Always await**: Every Playwright action should be awaited
|
|
2. **No hardcoded waits**: Use `waitForSelector`, `waitForLoadState`, not `waitForTimeout`
|
|
3. **Wait for elements**: Explicitly wait for elements before interaction
|
|
4. **Wait for navigation**: Use `waitForURL` after actions that navigate
|
|
|
|
### Assertions
|
|
1. **Explicit expectations**: Use `expect()` with specific matchers
|
|
2. **Wait for conditions**: Assertions automatically wait (default 5s)
|
|
3. **Multiple assertions**: It's OK to have multiple assertions per test
|
|
4. **Negative assertions**: Use `.not.toBeVisible()` for negative cases
|
|
|
|
### Error Handling
|
|
1. **Screenshot on failure**: Configure in playwright.config.ts
|
|
2. **Trace on retry**: Enable trace recording for debugging
|
|
3. **Meaningful errors**: Assertions should provide clear error messages
|
|
4. **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')` or `page.locator('#id')`
|
|
- Never use `page.getByRole()`, `page.getByText()`, or `page.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 structure
|
|
- `playwright.config.ts` - Recommended Playwright configuration
|
|
- `fixtures.ts` - Custom fixture examples (authentication, data setup)
|
|
- `utils.ts` - Helper functions for common operations
|