Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:28:25 +08:00
commit bc292955bb
23 changed files with 5385 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
{
"name": "playwright-e2e",
"description": "Comprehensive Playwright-based E2E testing plugin with test generation, Page Object Models, debugging, and maintenance capabilities. Follows best practices including data-testid locators and TypeScript-first approach.",
"version": "1.0.0",
"author": {
"name": "Claude Code Marketplace"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# playwright-e2e
Comprehensive Playwright-based E2E testing plugin with test generation, Page Object Models, debugging, and maintenance capabilities. Follows best practices including data-testid locators and TypeScript-first approach.

View File

@@ -0,0 +1,72 @@
# Create Page Object Command
## Description
Create a Page Object Model (POM) for a specific page or component. Generates a TypeScript class with locators and methods following the Page Object pattern with data-testid locators.
## Usage
```
/create-page-object [page-name]
```
## Parameters
- `page-name` - Name of the page or component (required)
## Examples
```
/create-page-object LoginPage
```
```
/create-page-object ProductDetailsPage
```
```
/create-page-object CheckoutForm
```
## Instructions for Claude
When this command is invoked:
1. **Invoke the page-object-builder skill** to handle the Page Object creation
2. **Gather information**:
- Page name and URL
- Key elements on the page
- Common user actions
- data-testid values for elements
3. **Generate Page Object class**:
- TypeScript class with proper types
- Readonly locators using data-testid
- Constructor accepting Page object
- goto() method for navigation
- Action methods (async)
- Getter methods for assertions
4. **Provide usage example**:
- Show how to use the Page Object in tests
- Demonstrate common actions
- Show assertion patterns
5. **List required data-testid values**:
- Document all testid values needed in the UI
- Provide semantic naming suggestions
## Error Handling
- If page name is missing, prompt for it
- If insufficient information, ask for page details
- Suggest data-testid names if not provided
- Warn if Page Object seems too large (consider splitting)
## Notes
- All locators must use data-testid
- Page Objects should not contain assertions
- Use getters for elements that need assertions
- Keep Page Objects focused on a single page/component

80
commands/debug-test.md Normal file
View File

@@ -0,0 +1,80 @@
# Debug Test Command
## Description
Debug a failing Playwright test by analyzing error messages, screenshots, and traces. Provides actionable solutions for common test failures including timeouts, selector issues, and race conditions.
## Usage
```
/debug-test [test-name-or-error]
```
## Parameters
- `test-name-or-error` - Test name, file path, or error message (optional)
## Examples
```
/debug-test login.spec.ts
```
```
/debug-test TimeoutError: waiting for selector
```
```
/debug-test Test "user dashboard loads" is flaky
```
## Instructions for Claude
When this command is invoked:
1. **Invoke the test-debugger skill** to handle debugging
2. **Gather failure information**:
- Error message and stack trace
- Test file location
- Expected vs actual behavior
- Screenshots/traces if available
- Failure frequency (always or intermittent)
3. **Analyze the error**:
- Identify error type (timeout, selector, assertion, etc.)
- Determine root cause
- Check for common issues (missing waits, wrong testid, race conditions)
4. **Optionally use Playwright MCP**:
- Navigate to the page if needed
- Inspect element state
- Test locator strategies
- Verify data-testid values
5. **Provide solution**:
- Explain what's wrong
- Show the fix with code examples
- Explain how to prevent in future
- Provide verification steps
6. **Apply the fix if requested**:
- Update the test code
- Add missing waits
- Fix locators
- Improve test stability
## Error Handling
- If insufficient information, ask for error details
- If test file not found, ask for correct path
- If error is unclear, use MCP to investigate
- If multiple issues, prioritize and fix one at a time
## Notes
- Most test failures are timing-related
- Always verify data-testid values are correct
- Use explicit waits, not hardcoded timeouts
- Check test isolation if flaky
- Run tests multiple times to verify fix

88
commands/fix-flaky.md Normal file
View File

@@ -0,0 +1,88 @@
# Fix Flaky Test Command
## Description
Analyze and fix flaky (intermittently failing) Playwright tests. Identifies race conditions, improves waiting strategies, and ensures test stability.
## Usage
```
/fix-flaky [test-name]
```
## Parameters
- `test-name` - Name or path of the flaky test (required)
## Examples
```
/fix-flaky dashboard.spec.ts
```
```
/fix-flaky "should load user profile"
```
```
/fix-flaky tests/checkout.spec.ts:42
```
## Instructions for Claude
When this command is invoked:
1. **Invoke the test-debugger and test-maintainer skills** for analysis
2. **Gather information**:
- Test file and name
- Failure pattern (how often it fails)
- Error messages when it fails
- Environment (local vs CI)
- Which step usually fails
3. **Identify flakiness causes**:
- Race conditions
- Missing waits
- Hardcoded timeouts
- Network dependencies
- Test isolation issues
- Environment differences
4. **Common fixes**:
- Add explicit waits before interactions
- Replace waitForTimeout() with proper waits
- Wait for network to settle
- Wait for specific API responses
- Increase timeouts for slow operations
- Improve test isolation
- Add retry configuration
5. **Apply improvements**:
- Update test code
- Add proper waits
- Fix race conditions
- Ensure test isolation
- Configure retries if needed
6. **Verify stability**:
- Run test multiple times (10+ times)
- Test in different environments
- Check in CI environment
- Monitor for continued flakiness
## Error Handling
- If test not found, ask for correct path
- If can't reproduce failure, ask for more details
- If multiple issues, fix most impactful first
- Warn if test design is fundamentally flaky
## Notes
- Flakiness is usually caused by timing issues
- Never use waitForTimeout() - use explicit waits
- Wait for network/animations to complete
- Ensure tests are isolated and don't share state
- Run tests 10+ times to verify fix
- Consider enabling retries for legitimately slow operations

81
commands/generate-test.md Normal file
View File

@@ -0,0 +1,81 @@
# Generate Test Command
## Description
Generate a production-ready Playwright E2E test from a natural language description. Creates TypeScript test files following best practices including data-testid locators, AAA pattern, and proper async/await usage.
## Usage
```
/generate-test [description]
```
## Parameters
- `description` - Natural language description of the test scenario (required)
## Examples
```
/generate-test Login flow with valid credentials
```
```
/generate-test Add item to cart and complete checkout
```
```
/generate-test Form validation for empty email field
```
## Instructions for Claude
When this command is invoked:
1. **Invoke the test-generator skill** to handle the test generation
2. **Gather requirements** from the user:
- Feature/functionality to test
- User flow or scenario
- Expected outcomes
- Page URLs involved
- data-testid values (or suggest them)
3. **Generate the test file** following these requirements:
- TypeScript file with `.spec.ts` extension
- Use only data-testid locators
- Follow AAA pattern (Arrange-Act-Assert)
- Include proper async/await
- Add explicit waits (no hardcoded timeouts)
- Include meaningful assertions
- Add comments for clarity
4. **Create supporting files if needed**:
- `playwright.config.ts` if first test
- Custom fixtures if needed
- Utility functions if reusable
5. **Provide usage instructions**:
- How to run the test
- What data-testid values need to be added to UI
- How to debug if it fails
6. **Validate the generated test**:
- Ensure only data-testid locators
- Check for proper waits
- Verify AAA structure
- Confirm TypeScript types
## Error Handling
- If description is too vague, ask clarifying questions
- If missing required information (URLs, element names), prompt user
- If data-testid values aren't provided, suggest semantic names
- Warn if the test scenario seems too complex for a single test
## Notes
- Tests should be focused and test one specific behavior
- Always use data-testid for locators (MANDATORY)
- Include explicit waits, never use waitForTimeout()
- Follow Playwright and TypeScript best practices
- Ensure tests are maintainable and readable

121
plugin.lock.json Normal file
View File

@@ -0,0 +1,121 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:joel611/claude-plugins:plugins/testing/playwright-e2e",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "9e2a3bd3cdb5847eb47693d097c4dcea033129d3",
"treeHash": "83a3277e6a0f0e86f75c5899db47cee3c89a1ef152a89da088e658da30859226",
"generatedAt": "2025-11-28T10:19:17.447534Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "playwright-e2e",
"description": "Comprehensive Playwright-based E2E testing plugin with test generation, Page Object Models, debugging, and maintenance capabilities. Follows best practices including data-testid locators and TypeScript-first approach.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "47b5a38e7b7d574483b0c76d5962f6b12df25afab339e8e74c72cac1e723da59"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "ff90d0374743dc61c490e9370146f155f773dbe23a7dd8242342e5b3976798e9"
},
{
"path": "commands/debug-test.md",
"sha256": "18d9938d993549595a335d5fb0e530f0b437ffcc8b112892a64ab05d8ed81585"
},
{
"path": "commands/create-page-object.md",
"sha256": "a34b07f02ca2c3b6cbbdaf9d35e8c19405b4788881b61a86221c2491d18f3115"
},
{
"path": "commands/fix-flaky.md",
"sha256": "195dda939e72bce8a81da833bcbe83b4809beae0c32995dc83454355f78713db"
},
{
"path": "commands/generate-test.md",
"sha256": "9a9ddaa36cb7bb6a2369c6b1ae2aab2d5e174cf4ceb5b2a37903abb5c70a2d00"
},
{
"path": "skills/page-object-builder/SKILL.md",
"sha256": "707beea0de7e2bd4767c8e9ee89160746334fbb2b00ee871466cef93f06058cf"
},
{
"path": "skills/page-object-builder/resources/page-template.ts",
"sha256": "6760681fcc11af32e953cbb396606078c81bb2fd9c23978cc9cef53e8339cafb"
},
{
"path": "skills/page-object-builder/resources/base-page.ts",
"sha256": "c86b44048ec379e06f904dbf559d273c6d3891baccc232ed09d572bc9a56526d"
},
{
"path": "skills/page-object-builder/resources/component-template.ts",
"sha256": "278684194537a956428744a39b19154d69216330c5846df0c2c3765fe5c7de04"
},
{
"path": "skills/test-generator/SKILL.md",
"sha256": "c37a0bcc1d8816ff7bf514cb26509c39d9ab39271b50f11c3c9b4b8ca353636a"
},
{
"path": "skills/test-generator/resources/fixtures.ts",
"sha256": "f6ab3e672f574790523d67886f593658dd56202d0f1c3a5f771ec81e762276e4"
},
{
"path": "skills/test-generator/resources/utils.ts",
"sha256": "fd9e8a4414f4dd735e26103f7efecbc96b6b6a44c9e0cd390ac5ef96c4242e6d"
},
{
"path": "skills/test-generator/resources/playwright.config.ts",
"sha256": "69160885d727fc9aa1a71920af11861b5738df089e987d9d644ab1889a8512f0"
},
{
"path": "skills/test-generator/resources/test-template.ts",
"sha256": "b8af62a704f7033ceac3419b3939e6600f495c1c5bf6923dea08ad5dcfe16e9c"
},
{
"path": "skills/test-maintainer/SKILL.md",
"sha256": "5baaa4667ab4846188bf88f1e4469ea864633caa6857da269ecb628cff36a160"
},
{
"path": "skills/test-maintainer/resources/best-practices.md",
"sha256": "31571196ec2c7289ceab0a3543bf25f0eaa5437f7cdb2124bb886f580e603cf5"
},
{
"path": "skills/test-maintainer/resources/refactoring-patterns.md",
"sha256": "264287952081819e36c48336a4dddf2c9d2a78103f1389a5692a5e61adb662f7"
},
{
"path": "skills/test-debugger/SKILL.md",
"sha256": "02e7a8ffb6bc6c63c50e7389f97350f24f2656bb05fbcb31221768d4748d2134"
},
{
"path": "skills/test-debugger/resources/playwright-commands.md",
"sha256": "27f583cacdf9c9c26fc7d63f7aa0106fb74ca373e19cbaa777f06240f9539683"
},
{
"path": "skills/test-debugger/resources/common-errors.md",
"sha256": "6279257dad9c59cd0e6f21294d12fa511a739ec9b9d2e664100ee5902dd659ce"
},
{
"path": "skills/test-debugger/resources/debugging-checklist.md",
"sha256": "8b7571a8105a34f0c6ba96f5da3c462c967f91dbe672535b8db411d8df9262d1"
}
],
"dirSha256": "83a3277e6a0f0e86f75c5899db47cee3c89a1ef152a89da088e658da30859226"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,727 @@
# Page Object Builder Skill
## Purpose
Create maintainable and reusable Page Object Models (POMs) for Playwright tests. Generates TypeScript classes that encapsulate page-specific locators and actions, following the Page Object Model design pattern with data-testid locators exclusively.
## When to Use This Skill
Use this skill when you need to:
- Create a Page Object Model for a specific page or component
- Refactor tests to use the POM pattern
- Build reusable page classes for complex applications
- Encapsulate page-specific logic and locators
- Improve test maintainability and reduce duplication
Do NOT use this skill when:
- Writing simple one-off tests (use test-generator skill)
- Debugging existing tests (use test-debugger skill)
- Refactoring existing POMs (use test-maintainer skill)
## Prerequisites
Before using this skill:
1. Understanding of the page structure and elements
2. Knowledge of user interactions on the page
3. List of data-testid values for page elements (or ability to suggest them)
4. Playwright installed in the project
5. Basic understanding of TypeScript classes
## Instructions
### Step 1: Identify Page Information
Gather from the user:
- **Page name** or component name
- **Page URL** or route
- **Key elements** on the page (buttons, inputs, text, etc.)
- **Common actions** users perform on the page
- **data-testid values** for all elements (or help define them)
### Step 2: Plan Page Object Structure
Determine:
- **Class name** (e.g., `LoginPage`, `DashboardPage`, `CheckoutPage`)
- **Properties**: Locators for all page elements
- **Methods**: Actions users can perform (login, addToCart, etc.)
- **Getters**: Read-only properties for assertions
- **Navigation**: How to reach this page
### Step 3: Create Page Object Class
Generate a TypeScript class with:
**Structure:**
```typescript
import { Page, Locator } from '@playwright/test';
export class PageName {
readonly page: Page;
// Locators
readonly elementName: Locator;
constructor(page: Page) {
this.page = page;
this.elementName = page.locator('[data-testid="element-name"]');
}
// Navigation
async goto() {
await this.page.goto('/page-url');
}
// Actions
async performAction() {
await this.elementName.click();
}
// Getters for assertions
getElement() {
return this.elementName;
}
}
```
**Key Requirements:**
1. All locators use data-testid (MANDATORY)
2. Locators are readonly properties
3. Constructor accepts Page object
4. Include goto() method for navigation
5. Action methods are async and return Promise<void>
6. Getter methods for elements that need assertions
7. Use TypeScript types
8. Add JSDoc comments for complex methods
### Step 4: Define Locators
For each element:
```typescript
readonly elementName: Locator;
constructor(page: Page) {
this.page = page;
this.elementName = page.locator('[data-testid="element-name"]');
}
```
**Naming Convention:**
- Use camelCase for properties
- Descriptive names (e.g., `submitButton`, `emailInput`, `errorMessage`)
- Suffix with element type when helpful (Button, Input, Message, Link)
### Step 5: Implement Action Methods
For each user action:
```typescript
/**
* Descriptive action name
* @param param - Parameter description if needed
*/
async actionName(param?: string): Promise<void> {
// Wait for element if needed
await this.element.waitFor({ state: 'visible' });
// Perform action
await this.element.click();
// or
await this.element.fill(param);
}
```
**Common Actions:**
- Form filling: `async fillForm(data: FormData)`
- Button clicks: `async clickButton()`
- Navigation: `async navigateTo(section: string)`
- Complex workflows: `async completeCheckout(details: CheckoutDetails)`
### Step 6: Add Getter Methods
For elements that tests will assert against:
```typescript
getElementName(): Locator {
return this.elementName;
}
async getTextContent(): Promise<string> {
return await this.element.textContent() || '';
}
async isElementVisible(): Promise<boolean> {
return await this.element.isVisible();
}
```
### Step 7: Validate Page Object
Ensure the Page Object includes:
- [ ] All locators use data-testid
- [ ] Locators are readonly
- [ ] All methods are async
- [ ] Constructor accepts Page
- [ ] goto() method for navigation
- [ ] JSDoc comments for public methods
- [ ] TypeScript types for parameters
- [ ] Meaningful method names
- [ ] No test assertions in POM (only actions and getters)
### Step 8: Provide Usage Example
Show how to use the Page Object in tests:
```typescript
import { test, expect } from '@playwright/test';
import { PageName } from './page-objects/PageName';
test('test description', async ({ page }) => {
const pageName = new PageName(page);
await pageName.goto();
await pageName.performAction();
await expect(pageName.getElement()).toBeVisible();
});
```
## Examples
### Example 1: Login Page Object
**Input:**
"Create a Page Object for the login page with username, password fields, login button, and error message."
**Output:**
```typescript
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for the Login Page
*
* Handles user authentication flow including:
* - Filling login credentials
* - Submitting the form
* - Accessing error messages
*/
export class LoginPage {
readonly page: Page;
// Locators
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('[data-testid="username-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.errorMessage = page.locator('[data-testid="error-message"]');
this.forgotPasswordLink = page.locator('[data-testid="forgot-password-link"]');
}
/**
* Navigate to the login page
*/
async goto(): Promise<void> {
await this.page.goto('/login');
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Perform login with credentials
* @param username - User's username or email
* @param password - User's password
*/
async login(username: string, password: string): Promise<void> {
await this.usernameInput.waitFor({ state: 'visible' });
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
/**
* Fill only the username field
*/
async fillUsername(username: string): Promise<void> {
await this.usernameInput.fill(username);
}
/**
* Fill only the password field
*/
async fillPassword(password: string): Promise<void> {
await this.passwordInput.fill(password);
}
/**
* Click the login button
*/
async clickLogin(): Promise<void> {
await this.loginButton.click();
}
/**
* Click forgot password link
*/
async clickForgotPassword(): Promise<void> {
await this.forgotPasswordLink.click();
}
/**
* Get the error message element for assertions
*/
getErrorMessage(): Locator {
return this.errorMessage;
}
/**
* Check if error message is visible
*/
async hasError(): Promise<boolean> {
try {
await this.errorMessage.waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
}
}
/**
* Get the text content of the error message
*/
async getErrorText(): Promise<string> {
const text = await this.errorMessage.textContent();
return text?.trim() || '';
}
}
```
**Usage:**
```typescript
import { test, expect } from '@playwright/test';
import { LoginPage } from './page-objects/LoginPage';
test.describe('Login Flow', () => {
test('should login successfully with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'SecurePass123');
// Assert navigation to dashboard
await page.waitForURL('/dashboard');
});
test('should show error with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@example.com', 'wrongpass');
// Assert error message
await expect(loginPage.getErrorMessage()).toBeVisible();
await expect(loginPage.getErrorMessage()).toContainText('Invalid credentials');
});
});
```
### Example 2: Product Page Object
**Input:**
"Create a Page Object for a product details page with product name, price, description, quantity selector, add to cart button, and reviews section."
**Output:**
```typescript
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for Product Details Page
*
* Handles product viewing and purchasing actions including:
* - Viewing product details
* - Selecting quantity
* - Adding to cart
* - Reading reviews
*/
export class ProductPage {
readonly page: Page;
// Product Information Locators
readonly productName: Locator;
readonly productPrice: Locator;
readonly productDescription: Locator;
readonly productImage: Locator;
// Purchase Locators
readonly quantityInput: Locator;
readonly addToCartButton: Locator;
readonly buyNowButton: Locator;
// Reviews Locators
readonly reviewsSection: Locator;
readonly reviewItems: Locator;
readonly averageRating: Locator;
// Additional Actions
readonly wishlistButton: Locator;
readonly shareButton: Locator;
constructor(page: Page) {
this.page = page;
// Product information
this.productName = page.locator('[data-testid="product-name"]');
this.productPrice = page.locator('[data-testid="product-price"]');
this.productDescription = page.locator('[data-testid="product-description"]');
this.productImage = page.locator('[data-testid="product-image"]');
// Purchase
this.quantityInput = page.locator('[data-testid="quantity-input"]');
this.addToCartButton = page.locator('[data-testid="add-to-cart-button"]');
this.buyNowButton = page.locator('[data-testid="buy-now-button"]');
// Reviews
this.reviewsSection = page.locator('[data-testid="reviews-section"]');
this.reviewItems = page.locator('[data-testid="review-item"]');
this.averageRating = page.locator('[data-testid="average-rating"]');
// Actions
this.wishlistButton = page.locator('[data-testid="wishlist-button"]');
this.shareButton = page.locator('[data-testid="share-button"]');
}
/**
* Navigate to a product page by ID
*/
async goto(productId: string): Promise<void> {
await this.page.goto(`/products/${productId}`);
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Set the quantity for purchase
*/
async setQuantity(quantity: number): Promise<void> {
await this.quantityInput.waitFor({ state: 'visible' });
await this.quantityInput.clear();
await this.quantityInput.fill(quantity.toString());
}
/**
* Add product to cart with specified quantity
*/
async addToCart(quantity = 1): Promise<void> {
if (quantity > 1) {
await this.setQuantity(quantity);
}
await this.addToCartButton.waitFor({ state: 'enabled' });
await this.addToCartButton.click();
}
/**
* Click Buy Now button
*/
async buyNow(): Promise<void> {
await this.buyNowButton.click();
}
/**
* Add product to wishlist
*/
async addToWishlist(): Promise<void> {
await this.wishlistButton.click();
}
/**
* Click share button
*/
async shareProduct(): Promise<void> {
await this.shareButton.click();
}
/**
* Get product name text
*/
async getProductName(): Promise<string> {
const text = await this.productName.textContent();
return text?.trim() || '';
}
/**
* Get product price text
*/
async getProductPrice(): Promise<string> {
const text = await this.productPrice.textContent();
return text?.trim() || '';
}
/**
* Get number of reviews
*/
async getReviewCount(): Promise<number> {
return await this.reviewItems.count();
}
/**
* Get average rating text
*/
async getAverageRating(): Promise<string> {
const text = await this.averageRating.textContent();
return text?.trim() || '';
}
// Getters for assertions
getProductNameElement(): Locator {
return this.productName;
}
getProductPriceElement(): Locator {
return this.productPrice;
}
getAddToCartButton(): Locator {
return this.addToCartButton;
}
getReviewsSection(): Locator {
return this.reviewsSection;
}
}
```
### Example 3: Dashboard Page with Multiple Sections
**Input:**
"Create a Page Object for a dashboard with header, sidebar navigation, main content area, and user profile dropdown."
**Output:**
```typescript
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for Dashboard Page
*
* Handles dashboard navigation and interactions including:
* - Sidebar navigation
* - User profile actions
* - Dashboard content
*/
export class DashboardPage {
readonly page: Page;
// Header Locators
readonly header: Locator;
readonly logo: Locator;
readonly searchBar: Locator;
readonly notificationIcon: Locator;
readonly userProfileDropdown: Locator;
// Sidebar Locators
readonly sidebar: Locator;
readonly homeLink: Locator;
readonly projectsLink: Locator;
readonly settingsLink: Locator;
readonly logoutButton: Locator;
// Main Content Locators
readonly mainContent: Locator;
readonly dashboardTitle: Locator;
readonly statsCards: Locator;
// Profile Dropdown Locators
readonly profileMenu: Locator;
readonly profileLink: Locator;
readonly accountSettingsLink: Locator;
constructor(page: Page) {
this.page = page;
// Header
this.header = page.locator('[data-testid="dashboard-header"]');
this.logo = page.locator('[data-testid="logo"]');
this.searchBar = page.locator('[data-testid="search-bar"]');
this.notificationIcon = page.locator('[data-testid="notification-icon"]');
this.userProfileDropdown = page.locator('[data-testid="user-profile-dropdown"]');
// Sidebar
this.sidebar = page.locator('[data-testid="sidebar"]');
this.homeLink = page.locator('[data-testid="nav-home"]');
this.projectsLink = page.locator('[data-testid="nav-projects"]');
this.settingsLink = page.locator('[data-testid="nav-settings"]');
this.logoutButton = page.locator('[data-testid="logout-button"]');
// Main Content
this.mainContent = page.locator('[data-testid="main-content"]');
this.dashboardTitle = page.locator('[data-testid="dashboard-title"]');
this.statsCards = page.locator('[data-testid="stat-card"]');
// Profile Dropdown
this.profileMenu = page.locator('[data-testid="profile-menu"]');
this.profileLink = page.locator('[data-testid="profile-link"]');
this.accountSettingsLink = page.locator('[data-testid="account-settings-link"]');
}
async goto(): Promise<void> {
await this.page.goto('/dashboard');
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Navigate using sidebar
*/
async navigateToHome(): Promise<void> {
await this.homeLink.click();
}
async navigateToProjects(): Promise<void> {
await this.projectsLink.click();
}
async navigateToSettings(): Promise<void> {
await this.settingsLink.click();
}
/**
* Search functionality
*/
async search(query: string): Promise<void> {
await this.searchBar.fill(query);
await this.page.keyboard.press('Enter');
}
/**
* Profile dropdown actions
*/
async openProfileDropdown(): Promise<void> {
await this.userProfileDropdown.click();
await this.profileMenu.waitFor({ state: 'visible' });
}
async navigateToProfile(): Promise<void> {
await this.openProfileDropdown();
await this.profileLink.click();
}
async navigateToAccountSettings(): Promise<void> {
await this.openProfileDropdown();
await this.accountSettingsLink.click();
}
async logout(): Promise<void> {
await this.logoutButton.click();
}
/**
* Get stats count
*/
async getStatsCount(): Promise<number> {
return await this.statsCards.count();
}
// Getters for assertions
getHeader(): Locator {
return this.header;
}
getSidebar(): Locator {
return this.sidebar;
}
getMainContent(): Locator {
return this.mainContent;
}
getDashboardTitle(): Locator {
return this.dashboardTitle;
}
}
```
## Best Practices
### Page Object Design
1. **Single Responsibility**: Each POM represents one page or component
2. **No Assertions**: POMs should not contain test assertions (use getters instead)
3. **Encapsulation**: Hide implementation details, expose high-level actions
4. **Reusability**: Design methods to be reused across multiple tests
5. **Clear Naming**: Use descriptive names for classes, properties, and methods
### Locator Management
1. **data-testid Only**: All locators must use data-testid attribute
2. **Readonly**: Declare all locators as readonly
3. **Initialize in Constructor**: All locators defined in constructor
4. **Descriptive Names**: Use meaningful names that describe the element
5. **Group Related**: Group related locators together (e.g., all form fields)
### Method Design
1. **Async Methods**: All action methods should be async
2. **Return Types**: Action methods return Promise<void>, getters return data
3. **Parameters**: Use TypeScript types for all parameters
4. **Documentation**: Add JSDoc comments for complex methods
5. **Atomic Actions**: Methods should perform single, focused actions
### Organization
1. **File Location**: Store POMs in `page-objects/` or `pages/` directory
2. **One Class Per File**: Each POM in its own file
3. **Export Class**: Export the class as default or named export
4. **Index File**: Consider creating index.ts for easier imports
5. **Naming Convention**: Use PascalCase with "Page" suffix
## Common Issues and Solutions
### Issue 1: Too Many Locators
**Problem:** Page Object has 30+ locators making it hard to maintain
**Solutions:**
- Break down into smaller component-based POMs
- Group related elements into sub-objects
- Consider component composition pattern
- Focus on elements actually used in tests
- Create separate POMs for complex sections
### Issue 2: Tests Still Break When UI Changes
**Problem:** Tests fail despite using POMs
**Solutions:**
- Ensure ONLY data-testid locators are used (not CSS/XPath)
- Coordinate with developers to keep data-testid stable
- Use semantic testid names that reflect purpose, not implementation
- Document all required data-testid values for developers
- Update POM centrally when testid changes
### Issue 3: Duplicate Code Across POMs
**Problem:** Same logic repeated in multiple Page Objects
**Solutions:**
- Extract common actions to utility functions
- Create base Page class with shared methods
- Use composition over inheritance when possible
- Create reusable components for common UI elements
- Consider creating a ComponentPage for shared components
### Issue 4: Methods Too Complex
**Problem:** Action methods contain complex logic and are hard to test
**Solutions:**
- Break down into smaller, atomic methods
- Extract complex logic to private helper methods
- Keep public methods simple and focused
- Use composition of smaller actions
- Add clear comments for multi-step workflows
### Issue 5: Hard to Test Page Objects
**Problem:** Can't verify Page Object behavior without full tests
**Solutions:**
- Keep POMs simple (locators + actions only)
- Avoid business logic in POMs
- Use type-safe interfaces
- Create example usage in comments
- Focus on thin wrappers over Playwright API
## Resources
The `resources/` directory contains templates for common patterns:
- `page-template.ts` - Basic Page Object structure
- `component-template.ts` - Component-based Page Object
- `base-page.ts` - Base class with common functionality

View File

@@ -0,0 +1,265 @@
import { Page, Locator } from '@playwright/test';
/**
* Base Page Object
*
* Contains common functionality shared across all page objects.
* Extend this class for your specific page objects to avoid duplication.
*
* Usage:
* ```typescript
* export class LoginPage extends BasePage {
* constructor(page: Page) {
* super(page);
* }
*
* async login() {
* await this.fillInput('username', 'user@example.com');
* }
* }
* ```
*/
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
/**
* Navigate to a URL
*/
async goto(url: string): Promise<void> {
await this.page.goto(url);
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Get locator by data-testid
*/
getByTestId(testId: string): Locator {
return this.page.locator(`[data-testid="${testId}"]`);
}
/**
* Click element by data-testid
*/
async clickByTestId(testId: string): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'visible' });
await locator.click();
}
/**
* Fill input by data-testid
*/
async fillInput(testId: string, value: string): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'visible' });
await locator.fill(value);
}
/**
* Get text content by data-testid
*/
async getTextByTestId(testId: string): Promise<string> {
const locator = this.getByTestId(testId);
const text = await locator.textContent();
return text?.trim() || '';
}
/**
* Check if element is visible by data-testid
*/
async isVisibleByTestId(testId: string): Promise<boolean> {
try {
await this.getByTestId(testId).waitFor({
state: 'visible',
timeout: 2000,
});
return true;
} catch {
return false;
}
}
/**
* Wait for element by data-testid
*/
async waitForElement(testId: string, timeout = 10000): Promise<Locator> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'visible', timeout });
return locator;
}
/**
* Wait for element to disappear
*/
async waitForElementToDisappear(
testId: string,
timeout = 10000
): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'hidden', timeout });
}
/**
* Fill a form with multiple fields
*/
async fillForm(fields: Record<string, string>): Promise<void> {
for (const [testId, value] of Object.entries(fields)) {
await this.fillInput(testId, value);
}
}
/**
* Click and wait for navigation
*/
async clickAndNavigate(
testId: string,
expectedUrl?: string | RegExp
): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'visible' });
await Promise.all([
expectedUrl
? this.page.waitForURL(expectedUrl)
: this.page.waitForLoadState('networkidle'),
locator.click(),
]);
}
/**
* Select option from dropdown
*/
async selectOption(testId: string, value: string): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'visible' });
await locator.selectOption(value);
}
/**
* Check/uncheck checkbox
*/
async toggleCheckbox(testId: string, checked: boolean): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'visible' });
const isChecked = await locator.isChecked();
if (isChecked !== checked) {
await locator.click();
}
}
/**
* Upload file
*/
async uploadFile(testId: string, filePath: string): Promise<void> {
const locator = this.getByTestId(testId);
await locator.waitFor({ state: 'attached' });
await locator.setInputFiles(filePath);
}
/**
* Get all text contents from multiple elements
*/
async getAllTextsByTestId(testId: string): Promise<string[]> {
const locator = this.getByTestId(testId);
await locator.first().waitFor({ state: 'visible' });
return await locator.allTextContents();
}
/**
* Get element count by data-testid
*/
async getCountByTestId(testId: string): Promise<number> {
return await this.getByTestId(testId).count();
}
/**
* Wait for API response
*/
async waitForApiResponse<T = any>(
urlPattern: string | RegExp,
action: () => Promise<void>
): Promise<T> {
const [response] = await Promise.all([
this.page.waitForResponse(urlPattern),
action(),
]);
return await response.json();
}
/**
* Get current URL
*/
getCurrentUrl(): string {
return this.page.url();
}
/**
* Reload page
*/
async reload(): Promise<void> {
await this.page.reload();
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Go back in browser history
*/
async goBack(): Promise<void> {
await this.page.goBack();
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Go forward in browser history
*/
async goForward(): Promise<void> {
await this.page.goForward();
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Take screenshot
*/
async takeScreenshot(name: string): Promise<void> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
await this.page.screenshot({
path: `${name}-${timestamp}.png`,
fullPage: true,
});
}
}
/**
* Example usage:
*
* export class LoginPage extends BasePage {
* readonly usernameInput: Locator;
* readonly passwordInput: Locator;
* readonly loginButton: Locator;
*
* constructor(page: Page) {
* super(page);
* this.usernameInput = this.getByTestId('username-input');
* this.passwordInput = this.getByTestId('password-input');
* this.loginButton = this.getByTestId('login-button');
* }
*
* async goto(): Promise<void> {
* await super.goto('/login');
* }
*
* async login(username: string, password: string): Promise<void> {
* await this.fillForm({
* 'username-input': username,
* 'password-input': password,
* });
* await this.clickAndNavigate('login-button', '/dashboard');
* }
* }
*/

View File

@@ -0,0 +1,161 @@
import { Page, Locator } from '@playwright/test';
/**
* Component Object Model for [Component Name]
*
* Description: Reusable component that appears on multiple pages
* Example: Navigation bar, modal dialog, toast notification, etc.
*
* Usage:
* ```typescript
* const component = new ComponentName(page);
* await component.performAction();
* ```
*/
export class ComponentName {
readonly page: Page;
// Component container
readonly container: Locator;
// Component elements
readonly element1: Locator;
readonly element2: Locator;
/**
* Constructor: Initialize component with page and optional container selector
* @param page - Playwright Page object
* @param containerTestId - Optional data-testid for component container
*/
constructor(page: Page, containerTestId = 'component-container') {
this.page = page;
this.container = page.locator(`[data-testid="${containerTestId}"]`);
// Scoped locators within the component
this.element1 = this.container.locator('[data-testid="element-1"]');
this.element2 = this.container.locator('[data-testid="element-2"]');
}
/**
* Wait for component to be visible
*/
async waitForComponent(): Promise<void> {
await this.container.waitFor({ state: 'visible' });
}
/**
* Check if component is visible
*/
async isVisible(): Promise<boolean> {
try {
await this.container.waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
}
}
/**
* Component-specific action
*/
async performAction(): Promise<void> {
await this.waitForComponent();
await this.element1.click();
}
// Getters
getContainer(): Locator {
return this.container;
}
}
/**
* Example: Modal Dialog Component
*/
export class ModalDialog {
readonly page: Page;
readonly modal: Locator;
readonly title: Locator;
readonly closeButton: Locator;
readonly confirmButton: Locator;
readonly cancelButton: Locator;
constructor(page: Page) {
this.page = page;
this.modal = page.locator('[data-testid="modal-dialog"]');
this.title = this.modal.locator('[data-testid="modal-title"]');
this.closeButton = this.modal.locator('[data-testid="modal-close"]');
this.confirmButton = this.modal.locator('[data-testid="modal-confirm"]');
this.cancelButton = this.modal.locator('[data-testid="modal-cancel"]');
}
async waitForModal(): Promise<void> {
await this.modal.waitFor({ state: 'visible' });
}
async close(): Promise<void> {
await this.closeButton.click();
await this.modal.waitFor({ state: 'hidden' });
}
async confirm(): Promise<void> {
await this.confirmButton.click();
await this.modal.waitFor({ state: 'hidden' });
}
async cancel(): Promise<void> {
await this.cancelButton.click();
await this.modal.waitFor({ state: 'hidden' });
}
async getTitle(): Promise<string> {
const text = await this.title.textContent();
return text?.trim() || '';
}
getModal(): Locator {
return this.modal;
}
}
/**
* Example: Navigation Bar Component
*/
export class NavigationBar {
readonly page: Page;
readonly nav: Locator;
readonly logo: Locator;
readonly menuItems: Locator;
readonly userMenu: Locator;
readonly searchBar: Locator;
constructor(page: Page) {
this.page = page;
this.nav = page.locator('[data-testid="navbar"]');
this.logo = this.nav.locator('[data-testid="logo"]');
this.menuItems = this.nav.locator('[data-testid="menu-item"]');
this.userMenu = this.nav.locator('[data-testid="user-menu"]');
this.searchBar = this.nav.locator('[data-testid="search-bar"]');
}
async clickLogo(): Promise<void> {
await this.logo.click();
}
async clickMenuItem(name: string): Promise<void> {
await this.menuItems.filter({ hasText: name }).click();
}
async openUserMenu(): Promise<void> {
await this.userMenu.click();
}
async search(query: string): Promise<void> {
await this.searchBar.fill(query);
await this.page.keyboard.press('Enter');
}
getNav(): Locator {
return this.nav;
}
}

View File

@@ -0,0 +1,112 @@
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for [Page Name]
*
* Description: [Brief description of what this page represents]
*
* Key functionality:
* - [Key function 1]
* - [Key function 2]
* - [Key function 3]
*/
export class PageName {
readonly page: Page;
// ============================================
// LOCATORS
// ============================================
// Group related locators together with comments
// Section 1: [e.g., Form Elements]
readonly elementName: Locator;
readonly anotherElement: Locator;
// Section 2: [e.g., Navigation]
readonly navElement: Locator;
/**
* Constructor: Initialize page object with locators
* @param page - Playwright Page object
*/
constructor(page: Page) {
this.page = page;
// Initialize all locators using data-testid
this.elementName = page.locator('[data-testid="element-name"]');
this.anotherElement = page.locator('[data-testid="another-element"]');
this.navElement = page.locator('[data-testid="nav-element"]');
}
// ============================================
// NAVIGATION
// ============================================
/**
* Navigate to this page
*/
async goto(): Promise<void> {
await this.page.goto('/page-url');
await this.page.waitForLoadState('domcontentloaded');
}
// ============================================
// ACTIONS
// ============================================
/**
* Perform a specific action
* @param param - Description of parameter
*/
async performAction(param: string): Promise<void> {
await this.elementName.waitFor({ state: 'visible' });
await this.elementName.click();
}
/**
* Fill a form with data
* @param data - Form data object
*/
async fillForm(data: { field1: string; field2: string }): Promise<void> {
await this.elementName.fill(data.field1);
await this.anotherElement.fill(data.field2);
}
// ============================================
// GETTERS (for assertions in tests)
// ============================================
/**
* Get element for assertions
*/
getElement(): Locator {
return this.elementName;
}
/**
* Get text content of an element
*/
async getTextContent(): Promise<string> {
const text = await this.elementName.textContent();
return text?.trim() || '';
}
/**
* Check if element is visible
*/
async isElementVisible(): Promise<boolean> {
try {
await this.elementName.waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
}
}
}
/**
* Required data-testid values:
* - element-name: [Description of element]
* - another-element: [Description of element]
* - nav-element: [Description of element]
*/

View File

@@ -0,0 +1,498 @@
# Test Debugger Skill
## Purpose
Debug failing Playwright E2E tests by analyzing error messages, stack traces, screenshots, and Playwright traces. Provides actionable solutions for common test failures including timeouts, selector issues, race conditions, and unexpected behaviors. Optionally uses Playwright MCP for live debugging.
## When to Use This Skill
Use this skill when you need to:
- Fix failing or flaky Playwright tests
- Understand why a test is timing out
- Debug element selector issues
- Analyze test traces and screenshots
- Resolve race conditions
- Investigate unexpected test behavior
- Use Playwright MCP to inspect live browser state
Do NOT use this skill when:
- Creating new tests (use test-generator skill)
- Building Page Objects (use page-object-builder skill)
- Refactoring test code (use test-maintainer skill)
## Prerequisites
Before using this skill:
1. Failing test file or error message
2. Test execution output or logs
3. Optional: Screenshots from test failure
4. Optional: Playwright trace file
5. Optional: Playwright MCP access for live debugging
## Instructions
### Step 1: Gather Failure Information
Ask the user for:
- **Error message** and stack trace
- **Test file** location and test name
- **Expected vs actual** behavior
- **Screenshots** (if available)
- **Trace file** path (if available)
- **Frequency**: Does it fail always or intermittently (flaky)?
- **Environment**: Local, CI, specific browser?
### Step 2: Analyze the Error
Identify the error type:
**Timeout Errors:**
- `Timeout 30000ms exceeded`
- `waiting for selector`
- `waiting for navigation`
**Selector Errors:**
- `Element not found`
- `strict mode violation`
- `No node found`
**Assertion Errors:**
- `Expected ... but received ...`
- `toBe`, `toContain`, `toBeVisible` failures
**Navigation Errors:**
- `Target closed`
- `Navigation failed`
- `ERR_CONNECTION_REFUSED`
**Race Conditions:**
- Intermittent failures
- Works locally but fails in CI
- Different results on different runs
### Step 3: Use Playwright MCP (Optional)
If Playwright MCP is available and needed:
- Use MCP tools to inspect browser state
- Navigate to the problematic page
- Check element visibility and attributes
- Verify data-testid values exist
- Test locator strategies
- Capture screenshots
### Step 4: Identify Root Cause
Common root causes:
**1. Missing or Wrong data-testid:**
- Element has different testid than expected
- testid doesn't exist in HTML
- Multiple elements with same testid
**2. Timing Issues:**
- Element not yet loaded when accessed
- No explicit wait before interaction
- Network requests still pending
- Animations or transitions in progress
**3. Element State:**
- Element exists but not visible
- Element disabled or not clickable
- Element covered by another element
- Element in different frame/iframe
**4. Test Isolation:**
- Tests depend on each other
- Shared state between tests
- Cleanup not performed
- Browser context pollution
**5. Environment Differences:**
- Different viewport sizes
- Different network speeds
- CI vs local differences
- Browser-specific issues
### Step 5: Provide Solution
For each root cause, provide:
1. **Explanation**: What's wrong
2. **Fix**: Code changes needed
3. **Prevention**: How to avoid in future
4. **Verification**: How to confirm it's fixed
### Step 6: Apply the Fix
**For Selector Issues:**
```typescript
// ❌ Before
await page.locator('[data-testid="wrong-id"]').click();
// ✅ After
await page.locator('[data-testid="correct-id"]').click();
```
**For Timeout Issues:**
```typescript
// ❌ Before
await page.locator('[data-testid="submit"]').click();
// ✅ After
await page.locator('[data-testid="submit"]').waitFor({ state: 'visible' });
await page.locator('[data-testid="submit"]').click();
```
**For Race Conditions:**
```typescript
// ❌ Before
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="result"]')).toBeVisible();
// ✅ After
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="result"]')).toBeVisible();
```
### Step 7: Verify the Fix
Guide the user to:
1. Run the test locally multiple times (3-5 times)
2. Check if error is resolved
3. Verify test passes consistently
4. Run related tests to ensure no regression
5. Consider running in CI if flakiness was CI-specific
## Examples
### Example 1: Timeout Error
**Input:**
```
Test failed with error:
TimeoutError: Timeout 30000ms exceeded.
=========================== logs ===========================
waiting for locator('[data-testid="submit-button"]')
```
**Analysis:**
- Timeout error waiting for element
- Element may not be loading
- Selector may be incorrect
**Solution:**
```typescript
// Check if the data-testid is correct in the HTML
// Add explicit wait with better error message
await page.locator('[data-testid="submit-button"]').waitFor({
state: 'visible',
timeout: 30000
});
// If still failing, verify element exists in DOM
const exists = await page.locator('[data-testid="submit-button"]').count();
console.log(`Submit button count: ${exists}`); // Should be 1
// Check if page has loaded
await page.waitForLoadState('domcontentloaded');
// Final solution
await page.waitForLoadState('domcontentloaded');
await page.locator('[data-testid="submit-button"]').waitFor({ state: 'visible' });
await page.locator('[data-testid="submit-button"]').click();
```
**Prevention:**
- Always add explicit waits before interactions
- Verify data-testid values in HTML
- Use `waitForLoadState` after navigation
### Example 2: Element Not Found
**Input:**
```
Error: Element not found
locator.click: Target closed
locator('[data-testid="user-menu"]')
```
**Analysis:**
- Element may not exist in current page state
- Possible strict mode violation (multiple elements)
- Element may be in a different frame
**Solution:**
```typescript
// Step 1: Verify element exists
const count = await page.locator('[data-testid="user-menu"]').count();
console.log(`Found ${count} elements`);
// If count = 0: Element doesn't exist, check data-testid in HTML
// If count > 1: Multiple elements, need to be more specific
// Step 2: If multiple elements, use .first() or filter
await page.locator('[data-testid="user-menu"]').first().click();
// Step 3: If in iframe, switch to frame first
const frame = page.frameLocator('[data-testid="app-frame"]');
await frame.locator('[data-testid="user-menu"]').click();
// Step 4: Add proper wait
await page.locator('[data-testid="user-menu"]').waitFor({ state: 'attached' });
await page.locator('[data-testid="user-menu"]').click();
```
**Prevention:**
- Ensure data-testid values are unique on the page
- Check for elements in frames/iframes
- Add waits before interaction
### Example 3: Flaky Test (Passes Sometimes)
**Input:**
```
Test fails intermittently:
- Passes 70% of the time locally
- Fails 90% of the time in CI
Error: expect(received).toContainText(expected)
Expected substring: "Success"
Received string: ""
```
**Analysis:**
- Classic race condition
- Element loads but content not yet populated
- Likely caused by async data fetching
- CI is slower so more likely to fail
**Solution:**
```typescript
// ❌ Before: No wait for content
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="message"]')).toContainText('Success');
// ✅ After: Wait for specific condition
await page.locator('[data-testid="submit"]').click();
// Option 1: Wait for network to settle
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="message"]')).toContainText('Success');
// Option 2: Wait for specific API call
await Promise.all([
page.waitForResponse('**/api/submit'),
page.locator('[data-testid="submit"]').click()
]);
await expect(page.locator('[data-testid="message"]')).toContainText('Success');
// Option 3: Use Playwright's auto-waiting in assertion
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="message"]')).toContainText('Success', {
timeout: 10000 // Explicit timeout for slow operations
});
```
**Prevention:**
- Wait for network requests to complete
- Use explicit timeouts for slow operations
- Run tests multiple times to catch flakiness
- Enable retries in playwright.config.ts
### Example 4: Assertion Failure
**Input:**
```
Error: expect(received).toBeVisible()
locator('[data-testid="success-message"]')
Expected: visible
Received: hidden
```
**Analysis:**
- Element exists but is hidden
- May need to wait for element to appear
- Check if element has conditional visibility
- Verify test logic is correct
**Solution:**
```typescript
// Step 1: Verify element exists
const exists = await page.locator('[data-testid="success-message"]').count();
console.log(`Element count: ${exists}`);
// Step 2: Check element state
const isVisible = await page.locator('[data-testid="success-message"]').isVisible();
console.log(`Is visible: ${isVisible}`);
// Step 3: Wait for visibility with timeout
await page.locator('[data-testid="success-message"]').waitFor({
state: 'visible',
timeout: 10000
});
// Step 4: If still not visible, check CSS
const display = await page.locator('[data-testid="success-message"]').evaluate(
el => window.getComputedStyle(el).display
);
console.log(`Display property: ${display}`);
// Step 5: Final solution
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible({
timeout: 10000
});
```
**Prevention:**
- Add explicit waits for elements that appear after actions
- Verify success conditions in the application logic
- Use appropriate timeout values
### Example 5: Using Playwright MCP for Debugging
**Input:**
"My test is failing but I can't figure out why. The error says element not found but I see it in the screenshot."
**Solution (Using MCP):**
```
1. Use Playwright MCP to navigate to the page:
- Navigate to the page where test fails
- Take screenshot to verify page state
2. Use MCP to check if element exists:
- Use MCP to find elements by data-testid
- Check how many elements match
- Inspect element attributes
3. Use MCP to test the locator:
- Try different locator strategies
- Check element visibility
- Verify element is in correct frame
4. Based on MCP findings, update the test:
- If element has different testid: Update locator
- If element in iframe: Add frame handling
- If multiple matches: Make locator more specific
```
## Best Practices
### Debugging Process
1. **Read error carefully**: Error messages are usually accurate
2. **Check test in isolation**: Run the single failing test
3. **Use debugging tools**: Screenshots, traces, MCP
4. **Add console logs**: Temporary logs to understand state
5. **Verify assumptions**: Check that data-testid values are correct
6. **Test incrementally**: Fix one issue at a time
### Common Debugging Techniques
1. **Add explicit waits**: Most failures are timing-related
2. **Check element count**: Verify unique selectors
3. **Use page.pause()**: Interactive debugging mode
4. **Enable headed mode**: See what's happening visually
5. **Slow motion**: Add `slowMo` in config to slow down actions
6. **Check traces**: Use Playwright trace viewer
### Preventing Future Issues
1. **Always use data-testid**: Stable locators
2. **Add explicit waits**: Don't rely on auto-waiting alone
3. **Test isolation**: Each test should be independent
4. **Proper cleanup**: Reset state between tests
5. **Handle async**: Wait for network/animations
6. **Run multiple times**: Catch flaky tests early
## Common Issues and Solutions
### Issue 1: Test Passes Locally but Fails in CI
**Problem:** Test works on developer machine but fails in CI environment
**Solutions:**
- **Viewport difference**: CI may use different screen size
```typescript
await page.setViewportSize({ width: 1920, height: 1080 });
```
- **Slower CI**: Increase timeouts for CI
```typescript
timeout: process.env.CI ? 60000 : 30000
```
- **Headless issues**: Test in headless mode locally
```bash
npx playwright test --headed=false
```
- **Network speed**: Add retries in config for CI
### Issue 2: Cannot Find Element with Correct data-testid
**Problem:** Selector looks correct but element not found
**Solutions:**
- Check element is in main page, not iframe
- Verify element is not dynamically loaded later
- Check for typos in data-testid value
- Use Playwright MCP to inspect actual HTML
- Add wait for element to be added to DOM
```typescript
await page.waitForSelector('[data-testid="element"]');
```
### Issue 3: Test Works First Time but Fails on Reruns
**Problem:** First run passes, subsequent runs fail
**Solutions:**
- **State leaking**: Tests aren't isolated
- Use `test.beforeEach` to reset state
- Use fixtures for clean context
- **Storage persistence**: Clear local storage/cookies
```typescript
await page.context().clearCookies();
await page.evaluate(() => localStorage.clear());
```
- **Database state**: Reset test database between runs
### Issue 4: Element Found but Click Doesn't Work
**Problem:** `element.click()` doesn't do anything or throws error
**Solutions:**
- **Element covered**: Another element is covering it
```typescript
await page.locator('[data-testid="modal-close"]').click({ force: true });
```
- **Element disabled**: Check if element is enabled
```typescript
await expect(page.locator('[data-testid="submit"]')).toBeEnabled();
```
- **Wrong element**: Multiple elements match, clicking wrong one
```typescript
await page.locator('[data-testid="item"]').first().click();
```
- **Animation in progress**: Wait for animations to complete
```typescript
await page.waitForTimeout(500); // Avoid this
// Better: Wait for element to be stable
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
await page.waitForLoadState('networkidle');
```
### Issue 5: Assertion Timing Out
**Problem:** `expect()` assertion times out after 5 seconds
**Solutions:**
- **Increase timeout**: For slow operations
```typescript
await expect(locator).toBeVisible({ timeout: 15000 });
```
- **Wait for condition**: Add wait before assertion
```typescript
await page.waitForLoadState('networkidle');
await expect(locator).toBeVisible();
```
- **Wrong expectation**: Verify what you're asserting is correct
- Check expected text/value in application
- Verify element actually appears on success
## Resources
The `resources/` directory contains helpful references:
- `debugging-checklist.md` - Step-by-step debugging guide
- `common-errors.md` - List of common errors and quick fixes
- `playwright-commands.md` - Useful Playwright debugging commands

View File

@@ -0,0 +1,378 @@
# Common Playwright Errors and Quick Fixes
## Timeout Errors
### Error: "Timeout 30000ms exceeded waiting for selector"
**Cause:** Element not found within timeout period
**Quick Fixes:**
```typescript
// 1. Add explicit wait
await page.waitForSelector('[data-testid="element"]', { state: 'visible' });
// 2. Verify data-testid is correct
const count = await page.locator('[data-testid="element"]').count();
console.log('Found', count, 'elements');
// 3. Wait for page to load first
await page.waitForLoadState('domcontentloaded');
// 4. Increase timeout if legitimately slow
await page.locator('[data-testid="element"]').waitFor({
state: 'visible',
timeout: 60000
});
```
### Error: "Timeout 30000ms exceeded waiting for navigation"
**Cause:** Page navigation taking too long or not happening
**Quick Fixes:**
```typescript
// 1. Wait for specific URL
await page.waitForURL('/expected-path', { timeout: 45000 });
// 2. Wait for load state
await page.waitForLoadState('networkidle');
// 3. Check if navigation actually occurs
console.log('Current URL:', page.url());
```
## Selector Errors
### Error: "strict mode violation: locator resolved to X elements"
**Cause:** Multiple elements match the selector
**Quick Fixes:**
```typescript
// 1. Use .first() or .last()
await page.locator('[data-testid="item"]').first().click();
// 2. Use .nth() for specific element
await page.locator('[data-testid="item"]').nth(2).click();
// 3. Make selector more specific (avoid if possible)
await page.locator('[data-testid="item"][aria-selected="true"]').click();
// 4. Filter locators
await page.locator('[data-testid="item"]').filter({ hasText: 'Active' }).click();
```
### Error: "Element not found"
**Cause:** Element doesn't exist or wrong selector
**Quick Fixes:**
```typescript
// 1. Verify element exists
const exists = await page.locator('[data-testid="element"]').count() > 0;
console.log('Element exists:', exists);
// 2. Check for typos in data-testid
// Verify in HTML: <button data-testid="correct-id">
// 3. Wait for element to be added to DOM
await page.locator('[data-testid="element"]').waitFor({ state: 'attached' });
// 4. Check if in iframe
const frame = page.frameLocator('[data-testid="app-frame"]');
await frame.locator('[data-testid="element"]').click();
```
## Assertion Errors
### Error: "expect(received).toBeVisible() - Expected visible but received hidden"
**Cause:** Element exists but is not visible
**Quick Fixes:**
```typescript
// 1. Wait for element to become visible
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="element"]')).toBeVisible();
// 2. Check visibility with timeout
await expect(page.locator('[data-testid="element"]')).toBeVisible({
timeout: 10000
});
// 3. Debug visibility
const isVisible = await page.locator('[data-testid="element"]').isVisible();
console.log('Is visible:', isVisible);
```
### Error: "expect(received).toContainText() - Expected substring not found"
**Cause:** Element doesn't contain expected text or not loaded yet
**Quick Fixes:**
```typescript
// 1. Wait for text to appear
await expect(page.locator('[data-testid="element"]')).toContainText('Expected', {
timeout: 10000
});
// 2. Wait for network before checking
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="element"]')).toContainText('Expected');
// 3. Check actual text content
const text = await page.locator('[data-testid="element"]').textContent();
console.log('Actual text:', text);
```
## Click Errors
### Error: "locator.click: Target closed"
**Cause:** Element or page disappeared before click completed
**Quick Fixes:**
```typescript
// 1. Wait for element to be stable
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
await page.waitForTimeout(100); // Allow animations to settle
await page.locator('[data-testid="element"]').click();
// 2. Use force click if element is covered
await page.locator('[data-testid="element"]').click({ force: true });
// 3. Check if element is being removed
const count = await page.locator('[data-testid="element"]').count();
console.log('Element count before click:', count);
```
### Error: "Element is not clickable"
**Cause:** Element is disabled, covered, or not in viewport
**Quick Fixes:**
```typescript
// 1. Check if element is enabled
await expect(page.locator('[data-testid="button"]')).toBeEnabled();
await page.locator('[data-testid="button"]').click();
// 2. Scroll into view
await page.locator('[data-testid="element"]').scrollIntoViewIfNeeded();
await page.locator('[data-testid="element"]').click();
// 3. Wait for element to be clickable
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="element"]')).toBeEnabled();
await page.locator('[data-testid="element"]').click();
```
## Network Errors
### Error: "net::ERR_CONNECTION_REFUSED"
**Cause:** Server is not running or wrong URL
**Quick Fixes:**
```typescript
// 1. Verify server is running
// Check if dev server is started: npm run dev
// 2. Check baseURL in playwright.config.ts
use: {
baseURL: 'http://localhost:3000', // Correct port?
}
// 3. Use full URL if baseURL not set
await page.goto('http://localhost:3000/path');
```
### Error: "Navigation failed because page was closed"
**Cause:** Page closed prematurely during navigation
**Quick Fixes:**
```typescript
// 1. Don't close page too early
// Remove premature page.close() calls
// 2. Wait for navigation to complete
await page.goto('/path');
await page.waitForLoadState('domcontentloaded');
// 3. Check for popup blockers or redirects
```
## Fill/Type Errors
### Error: "Cannot type into a non-editable element"
**Cause:** Trying to fill a non-input element
**Quick Fixes:**
```typescript
// 1. Verify element is an input/textarea
const tagName = await page.locator('[data-testid="element"]').evaluate(
el => el.tagName
);
console.log('Tag name:', tagName); // Should be INPUT or TEXTAREA
// 2. Check if element is contenteditable
const isEditable = await page.locator('[data-testid="element"]').evaluate(
el => el.contentEditable
);
// 3. Use correct element selector
// Ensure data-testid is on the input, not its container
```
### Error: "Element is disabled"
**Cause:** Trying to interact with disabled element
**Quick Fixes:**
```typescript
// 1. Wait for element to be enabled
await expect(page.locator('[data-testid="input"]')).toBeEnabled();
await page.locator('[data-testid="input"]').fill('value');
// 2. Check why element is disabled
const isDisabled = await page.locator('[data-testid="input"]').isDisabled();
console.log('Is disabled:', isDisabled);
// 3. Ensure prerequisites are met (e.g., terms accepted)
await page.locator('[data-testid="terms-checkbox"]').check();
await page.locator('[data-testid="submit"]').click();
```
## Frame/Iframe Errors
### Error: "Frame was detached"
**Cause:** Interacting with iframe that was removed
**Quick Fixes:**
```typescript
// 1. Re-acquire frame reference
const frame = page.frameLocator('[data-testid="app-frame"]');
await frame.locator('[data-testid="element"]').click();
// 2. Wait for frame to be ready
await page.frameLocator('[data-testid="app-frame"]').locator('body').waitFor();
// 3. Check if frame exists
const frameCount = await page.frames().length;
console.log('Frame count:', frameCount);
```
## Race Condition Errors
### Error: Test passes sometimes, fails other times
**Cause:** Race condition / flaky test
**Quick Fixes:**
```typescript
// 1. Wait for network to settle
await page.waitForLoadState('networkidle');
// 2. Wait for specific API calls
await page.waitForResponse('**/api/data');
// 3. Add explicit waits
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
// 4. Increase timeouts
await expect(page.locator('[data-testid="result"]')).toBeVisible({
timeout: 15000
});
// 5. Enable retries in playwright.config.ts
retries: 2,
```
## Context/State Errors
### Error: "localStorage is not defined"
**Cause:** Accessing localStorage before page is loaded
**Quick Fixes:**
```typescript
// 1. Navigate to page first
await page.goto('/');
await page.evaluate(() => localStorage.setItem('key', 'value'));
// 2. Use context.addInitScript for early setup
await context.addInitScript(() => {
localStorage.setItem('key', 'value');
});
```
### Error: "Test passes in isolation but fails in suite"
**Cause:** Test pollution / shared state
**Quick Fixes:**
```typescript
// 1. Clear state in beforeEach
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});
// 2. Use test.describe.serial for dependent tests
test.describe.serial('Login flow', () => {
// Tests run in order
});
// 3. Create fresh context for each test
test.use({ storageState: undefined });
```
## Configuration Errors
### Error: "Cannot find module '@playwright/test'"
**Cause:** Playwright not installed
**Quick Fixes:**
```bash
# Install Playwright
npm install -D @playwright/test
# Install browsers
npx playwright install
```
### Error: "No tests found"
**Cause:** Test files not matching pattern
**Quick Fixes:**
```typescript
// In playwright.config.ts
testDir: './tests',
testMatch: '**/*.spec.ts', // Or *.test.ts
```
## Debugging Commands
```bash
# Run with UI mode
npx playwright test --ui
# Run in headed mode
npx playwright test --headed
# Debug specific test
npx playwright test --debug test-file.spec.ts
# Show trace
npx playwright show-trace trace.zip
# Generate code
npx playwright codegen localhost:3000
```

View File

@@ -0,0 +1,250 @@
# Playwright Test Debugging Checklist
## Initial Assessment
- [ ] Read the error message carefully
- [ ] Note the failing test name and location
- [ ] Check if test fails consistently or intermittently
- [ ] Identify which browser(s) are failing
- [ ] Check if failure is local only or also in CI
## Error Type Identification
### Timeout Errors
- [ ] Check if element exists with correct data-testid
- [ ] Verify page has loaded completely
- [ ] Add explicit wait before interaction
- [ ] Check if network requests are pending
- [ ] Increase timeout if operation is legitimately slow
### Selector Errors
- [ ] Verify data-testid in HTML matches test code
- [ ] Check if multiple elements have the same testid
- [ ] Ensure element is not in an iframe
- [ ] Confirm element is added to DOM (not removed)
- [ ] Use Playwright Inspector to test selector
### Assertion Errors
- [ ] Verify expected value is correct
- [ ] Check if element exists and is visible
- [ ] Add wait before assertion
- [ ] Check if content is populated asynchronously
- [ ] Increase assertion timeout if needed
### Navigation Errors
- [ ] Check if URL is correct
- [ ] Verify server is running
- [ ] Check for network issues
- [ ] Ensure proper wait after navigation
- [ ] Look for redirects or authentication requirements
## Element Investigation
- [ ] Count elements matching selector (should be 1)
```typescript
const count = await page.locator('[data-testid="element"]').count();
console.log('Element count:', count);
```
- [ ] Check element visibility
```typescript
const isVisible = await page.locator('[data-testid="element"]').isVisible();
console.log('Is visible:', isVisible);
```
- [ ] Check element state
```typescript
const isEnabled = await page.locator('[data-testid="element"]').isEnabled();
console.log('Is enabled:', isEnabled);
```
- [ ] Get element attributes
```typescript
const testId = await page.locator('[data-testid="element"]').getAttribute('data-testid');
console.log('data-testid:', testId);
```
## Timing Investigation
- [ ] Add waits before interactions
```typescript
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
```
- [ ] Wait for page load
```typescript
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle');
```
- [ ] Wait for specific API calls
```typescript
await page.waitForResponse('**/api/endpoint');
```
- [ ] Check if animations/transitions are in progress
```typescript
await page.waitForTimeout(500); // Only for testing, remove later
```
## Interactive Debugging
- [ ] Run test in headed mode
```bash
npx playwright test --headed
```
- [ ] Use debug mode
```bash
npx playwright test --debug
```
- [ ] Add `page.pause()` in test
```typescript
await page.pause(); // Pauses execution for manual inspection
```
- [ ] Enable slow motion
```typescript
// In playwright.config.ts
use: {
launchOptions: {
slowMo: 1000, // Slow down by 1 second
},
}
```
- [ ] Take screenshots for inspection
```typescript
await page.screenshot({ path: 'debug-screenshot.png', fullPage: true });
```
## Trace Analysis
- [ ] Enable trace recording
```typescript
// In playwright.config.ts
use: {
trace: 'on-first-retry',
}
```
- [ ] Open trace viewer
```bash
npx playwright show-trace trace.zip
```
- [ ] Check trace for:
- Network requests
- Console logs
- Screenshots at each step
- DOM snapshots
- Action timeline
## Test Isolation Check
- [ ] Run test in isolation (comment out other tests)
- [ ] Check if test passes when run alone
- [ ] Verify no shared state between tests
- [ ] Ensure proper cleanup in `afterEach`
- [ ] Check for global state mutations
## Environment Differences
### Local vs CI
- [ ] Run test in headless mode locally
```bash
npx playwright test --headed=false
```
- [ ] Check viewport size matches CI
```typescript
await page.setViewportSize({ width: 1920, height: 1080 });
```
- [ ] Compare timeouts (CI may need longer)
- [ ] Check if CI has different environment variables
### Browser Differences
- [ ] Test in all configured browsers
```bash
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit
```
- [ ] Check for browser-specific issues
- [ ] Verify CSS compatibility
- [ ] Test JavaScript feature support
## Common Quick Fixes
### Fix #1: Add Explicit Wait
```typescript
// Before
await page.locator('[data-testid="element"]').click();
// After
await page.locator('[data-testid="element"]').waitFor({ state: 'visible' });
await page.locator('[data-testid="element"]').click();
```
### Fix #2: Wait for Network
```typescript
// Before
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="result"]')).toBeVisible();
// After
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="result"]')).toBeVisible();
```
### Fix #3: Handle Multiple Elements
```typescript
// Before
await page.locator('[data-testid="item"]').click(); // Error if multiple
// After
await page.locator('[data-testid="item"]').first().click();
```
### Fix #4: Increase Timeout
```typescript
// Before
await expect(page.locator('[data-testid="slow-element"]')).toBeVisible();
// After
await expect(page.locator('[data-testid="slow-element"]')).toBeVisible({
timeout: 15000
});
```
### Fix #5: Check Element State
```typescript
// Before
await page.locator('[data-testid="button"]').click();
// After
await expect(page.locator('[data-testid="button"]')).toBeEnabled();
await page.locator('[data-testid="button"]').click();
```
## Verification
- [ ] Run test 5+ times to verify consistency
- [ ] Test in all browsers
- [ ] Run full test suite to check for regressions
- [ ] Test in CI environment
- [ ] Document the fix for future reference
## Prevention
- [ ] Add explicit waits where needed
- [ ] Use only data-testid locators
- [ ] Ensure test isolation
- [ ] Add retry logic in config
- [ ] Review and refactor flaky tests regularly
- [ ] Keep tests simple and focused
- [ ] Maintain good test hygiene (cleanup, fixtures)

View File

@@ -0,0 +1,480 @@
# Useful Playwright Debugging Commands
## Running Tests
```bash
# Run all tests
npx playwright test
# Run specific test file
npx playwright test login.spec.ts
# Run tests matching pattern
npx playwright test --grep "login"
# Run tests in specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit
# Run tests in headed mode (see browser)
npx playwright test --headed
# Run specific test by line number
npx playwright test login.spec.ts:42
# Run tests in parallel
npx playwright test --workers=4
# Run tests sequentially
npx playwright test --workers=1
```
## Debugging
```bash
# Debug mode (opens inspector)
npx playwright test --debug
# Debug specific test
npx playwright test login.spec.ts --debug
# UI Mode (interactive)
npx playwright test --ui
# Step through tests
npx playwright test --debug --headed
# Playwright Inspector
PWDEBUG=1 npx playwright test
```
## Code Generation
```bash
# Generate test code by recording actions
npx playwright codegen
# Generate code for specific URL
npx playwright codegen https://example.com
# Generate with specific device
npx playwright codegen --device="iPhone 12"
# Generate with authentication
npx playwright codegen --save-storage=auth.json
```
## Trace Viewing
```bash
# Show trace file
npx playwright show-trace trace.zip
# Open trace from test results
npx playwright show-trace test-results/login-test/trace.zip
# View trace with network data
npx playwright show-trace trace.zip --network
```
## Report Viewing
```bash
# Open HTML report
npx playwright show-report
# Open specific report
npx playwright show-report ./playwright-report
```
## Installation & Setup
```bash
# Install Playwright
npm init playwright@latest
# Install Playwright Test
npm install -D @playwright/test
# Install browsers
npx playwright install
# Install specific browser
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
# Install with dependencies (Linux)
npx playwright install --with-deps
# Update Playwright
npm install -D @playwright/test@latest
npx playwright install
```
## Configuration
```bash
# Run with custom config
npx playwright test --config=custom.config.ts
# List all projects
npx playwright test --list
# Show configuration
npx playwright show-config
```
## Useful Test Options
```bash
# Run tests with retries
npx playwright test --retries=3
# Set timeout
npx playwright test --timeout=60000
# Run tests with maximum failures
npx playwright test --max-failures=5
# Run only failed tests
npx playwright test --last-failed
# Update snapshots
npx playwright test --update-snapshots
# Ignore snapshots
npx playwright test --ignore-snapshots
```
## Environment Variables
```bash
# Enable debug logs
DEBUG=pw:api npx playwright test
# Enable verbose logging
DEBUG=* npx playwright test
# Set browser
BROWSER=firefox npx playwright test
# Set headed mode
HEADED=1 npx playwright test
# Disable parallel execution
PWTEST_PARALLEL=0 npx playwright test
```
## In-Test Debugging Commands
### Console Logging
```typescript
// Log to console
console.log('Debug message:', value);
// Log page URL
console.log('Current URL:', page.url());
// Log element count
const count = await page.locator('[data-testid="item"]').count();
console.log('Element count:', count);
// Log element text
const text = await page.locator('[data-testid="element"]').textContent();
console.log('Element text:', text);
// Log all text contents
const allText = await page.locator('[data-testid="item"]').allTextContents();
console.log('All texts:', allText);
```
### Page Pause
```typescript
// Pause test execution (opens inspector)
await page.pause();
// Pause on specific condition
if (someCondition) {
await page.pause();
}
```
### Screenshots
```typescript
// Take screenshot
await page.screenshot({ path: 'screenshot.png' });
// Full page screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Element screenshot
await page.locator('[data-testid="element"]').screenshot({ path: 'element.png' });
// Screenshot with timestamp
const timestamp = Date.now();
await page.screenshot({ path: `debug-${timestamp}.png` });
```
### Video Recording
```typescript
// In playwright.config.ts
use: {
video: 'on', // or 'retain-on-failure'
}
// In test
const path = await page.video()?.path();
console.log('Video saved at:', path);
```
### Trace Recording
```typescript
// Start tracing
await context.tracing.start({ screenshots: true, snapshots: true });
// Stop tracing and save
await context.tracing.stop({ path: 'trace.zip' });
// Or in playwright.config.ts
use: {
trace: 'on-first-retry', // or 'on' for all tests
}
```
### Evaluate JavaScript
```typescript
// Execute JavaScript in page context
const result = await page.evaluate(() => {
return document.title;
});
console.log('Page title:', result);
// With parameters
const result = await page.evaluate((text) => {
return document.body.textContent?.includes(text);
}, 'search term');
// Get element properties
const value = await page.locator('[data-testid="input"]').evaluate(
(el: HTMLInputElement) => el.value
);
```
### Wait Commands
```typescript
// Wait for timeout (avoid in production)
await page.waitForTimeout(1000);
// Wait for selector
await page.waitForSelector('[data-testid="element"]');
// Wait for URL
await page.waitForURL('/dashboard');
await page.waitForURL(/\/user\/\d+/);
// Wait for load state
await page.waitForLoadState('load');
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle');
// Wait for response
await page.waitForResponse('**/api/data');
await page.waitForResponse(response =>
response.url().includes('/api/') && response.status() === 200
);
// Wait for request
await page.waitForRequest('**/api/users');
// Wait for function
await page.waitForFunction(() => document.querySelectorAll('.item').length > 5);
// Wait for event
await page.waitForEvent('response');
await page.waitForEvent('request');
```
### Network Debugging
```typescript
// Listen to all requests
page.on('request', request =>
console.log('>>', request.method(), request.url())
);
// Listen to all responses
page.on('response', response =>
console.log('<<', response.status(), response.url())
);
// Listen to console messages
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Listen to page errors
page.on('pageerror', error => console.log('PAGE ERROR:', error));
// Intercept and log requests
await page.route('**/api/**', route => {
console.log('API Request:', route.request().url());
route.continue();
});
```
### State Inspection
```typescript
// Get all cookies
const cookies = await context.cookies();
console.log('Cookies:', cookies);
// Get localStorage
const localStorage = await page.evaluate(() => {
return JSON.stringify(window.localStorage);
});
console.log('LocalStorage:', localStorage);
// Get sessionStorage
const sessionStorage = await page.evaluate(() => {
return JSON.stringify(window.sessionStorage);
});
console.log('SessionStorage:', sessionStorage);
// Get viewport size
const viewport = page.viewportSize();
console.log('Viewport:', viewport);
```
### Element State Checks
```typescript
// Check visibility
const isVisible = await page.locator('[data-testid="element"]').isVisible();
console.log('Is visible:', isVisible);
// Check if enabled
const isEnabled = await page.locator('[data-testid="button"]').isEnabled();
console.log('Is enabled:', isEnabled);
// Check if checked
const isChecked = await page.locator('[data-testid="checkbox"]').isChecked();
console.log('Is checked:', isChecked);
// Get bounding box
const box = await page.locator('[data-testid="element"]').boundingBox();
console.log('Bounding box:', box);
// Get attribute
const value = await page.locator('[data-testid="input"]').getAttribute('value');
console.log('Value:', value);
// Get inner text
const text = await page.locator('[data-testid="element"]').innerText();
console.log('Inner text:', text);
// Get input value
const inputValue = await page.locator('[data-testid="input"]').inputValue();
console.log('Input value:', inputValue);
```
## Performance Testing
```bash
# Run with tracing
npx playwright test --trace=on
# Run with video
npx playwright test --video=on
# Slow down execution
npx playwright test --slow-mo=1000
```
## CI/CD Commands
```bash
# Run in CI mode
CI=1 npx playwright test
# Run with JUnit reporter
npx playwright test --reporter=junit
# Run with multiple reporters
npx playwright test --reporter=html,json,junit
# Run with sharding
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
```
## Test Filtering
```bash
# Run tests with specific tag
npx playwright test --grep @smoke
# Skip tests with tag
npx playwright test --grep-invert @skip
# Run tests in specific describe block
npx playwright test --grep "Login tests"
# Run only tests marked with test.only
# (Automatically done if test.only exists)
```
## Maintenance Commands
```bash
# Clean test output
rm -rf test-results/
rm -rf playwright-report/
# List installed browsers
npx playwright --version
# Check for browser updates
npx playwright install --dry-run
# Uninstall browsers
npx playwright uninstall
# Clear browser cache
npx playwright install --force
```
## Tips
1. **Use `--headed` for visual debugging**
```bash
npx playwright test --headed --project=chromium
```
2. **Use `--debug` to step through test**
```bash
npx playwright test login.spec.ts --debug
```
3. **Use `--ui` for interactive mode**
```bash
npx playwright test --ui
```
4. **Use `show-trace` to analyze failures**
```bash
npx playwright show-trace trace.zip
```
5. **Use `codegen` to learn selectors**
```bash
npx playwright codegen http://localhost:3000
```

View File

@@ -0,0 +1,377 @@
# 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

View File

@@ -0,0 +1,131 @@
import { test as base, Page } from '@playwright/test';
/**
* Custom Fixtures for Playwright Tests
*
* Fixtures provide a way to set up test preconditions and share
* common setup logic across tests. They ensure test isolation
* and reduce boilerplate code.
*/
// Define custom fixture types
type MyFixtures = {
authenticatedPage: Page;
testUser: {
email: string;
password: string;
name: string;
};
};
/**
* Extend the base test with custom fixtures
*/
export const test = base.extend<MyFixtures>({
/**
* Fixture: Authenticated Page
*
* Provides a page that is already logged in with a test user.
* Use this when you need to test features that require authentication.
*
* Usage:
* test('should view profile', async ({ authenticatedPage }) => {
* await authenticatedPage.goto('/profile');
* // Test authenticated features
* });
*/
authenticatedPage: async ({ page }, use) => {
// Setup: Perform login
await page.goto('/login');
// Wait for login form to be visible
await page.locator('[data-testid="login-form"]').waitFor();
// Fill in credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('TestPassword123');
// Submit login form
await page.locator('[data-testid="login-button"]').click();
// Wait for successful login (redirect to dashboard)
await page.waitForURL('/dashboard');
// Verify login success
await page.locator('[data-testid="user-menu"]').waitFor();
// Provide the authenticated page to the test
await use(page);
// Teardown: Logout (optional)
// await page.locator('[data-testid="logout-button"]').click();
},
/**
* Fixture: Test User
*
* Provides consistent test user data across tests.
* Use this when you need user credentials or information.
*
* Usage:
* test('should register user', async ({ page, testUser }) => {
* await page.locator('[data-testid="email"]').fill(testUser.email);
* });
*/
testUser: async ({}, use) => {
const user = {
email: 'test@example.com',
password: 'TestPassword123',
name: 'Test User',
};
await use(user);
},
});
export { expect } from '@playwright/test';
/**
* Additional Fixture Examples:
*
* 1. Database Setup Fixture:
* dbContext: async ({}, use) => {
* const db = await setupTestDatabase();
* await use(db);
* await db.cleanup();
* }
*
* 2. API Context Fixture:
* apiContext: async ({ playwright }, use) => {
* const context = await playwright.request.newContext({
* baseURL: 'https://api.example.com',
* extraHTTPHeaders: { 'Authorization': 'Bearer token' }
* });
* await use(context);
* await context.dispose();
* }
*
* 3. Test Data Fixture:
* testProduct: async ({}, use) => {
* const product = {
* name: 'Test Product',
* price: 99.99,
* sku: 'TEST-001'
* };
* await use(product);
* }
*
* 4. Viewport Fixture:
* mobileViewport: async ({ page }, use) => {
* await page.setViewportSize({ width: 375, height: 667 });
* await use(page);
* }
*
* 5. Mock Data Fixture:
* mockApiResponses: async ({ page }, use) => {
* await page.route('**/api/users', route => {
* route.fulfill({ json: [{ id: 1, name: 'Test User' }] });
* });
* await use(page);
* }
*/

View File

@@ -0,0 +1,106 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Playwright Configuration
*
* This configuration follows best practices for E2E testing:
* - Retries for flaky test handling
* - Parallel execution for speed
* - Screenshots and traces on failure
* - Multiple browser support
*/
export default defineConfig({
// Test directory
testDir: './tests',
// Maximum time one test can run
timeout: 30 * 1000,
// Run tests in parallel
fullyParallel: true,
// Fail the build on CI if you accidentally left test.only in the source code
forbidOnly: !!process.env.CI,
// Retry on CI only
retries: process.env.CI ? 2 : 1,
// Number of parallel workers
workers: process.env.CI ? 1 : undefined,
// Reporter to use
reporter: [
['html'],
['list'],
// Add JUnit reporter for CI
...(process.env.CI ? [['junit', { outputFile: 'test-results/junit.xml' }]] : []),
],
// Shared settings for all projects
use: {
// Base URL to use in actions like `await page.goto('/')`
baseURL: process.env.BASE_URL || 'http://localhost:3000',
// Collect trace when retrying the failed test
trace: 'on-first-retry',
// Take screenshot on failure
screenshot: 'only-on-failure',
// Capture video on first retry
video: 'retain-on-failure',
// Maximum time for each action (e.g., click, fill)
actionTimeout: 10 * 1000,
// Maximum time for navigation actions
navigationTimeout: 30 * 1000,
},
// Configure projects for major browsers
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Test against mobile viewports
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
// Test against branded browsers
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
// Run your local dev server before starting the tests
// webServer: {
// command: 'npm run dev',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
});

View File

@@ -0,0 +1,76 @@
import { test, expect } from '@playwright/test';
/**
* Test Suite: [Feature Name]
*
* Description: [Brief description of what this test suite covers]
*/
test.describe('[Feature Name]', () => {
/**
* Setup: Runs before each test in this describe block
*/
test.beforeEach(async ({ page }) => {
// Navigate to the starting page
await page.goto('/');
// Wait for page to be ready
await page.waitForLoadState('domcontentloaded');
});
/**
* Test: [Specific behavior being tested]
*
* Scenario: [User story or use case]
* Expected: [What should happen]
*/
test('should [expected behavior]', async ({ page }) => {
// ============================================
// ARRANGE: Setup test preconditions
// ============================================
// Navigate to specific page (if needed)
// await page.goto('/specific-page');
// Verify page is loaded
// await expect(page.locator('[data-testid="page-container"]')).toBeVisible();
// ============================================
// ACT: Perform the action being tested
// ============================================
// Interact with elements using data-testid
// await page.locator('[data-testid="input-field"]').fill('test value');
// await page.locator('[data-testid="submit-button"]').click();
// ============================================
// ASSERT: Verify the expected outcome
// ============================================
// Check that the expected result is visible
// await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// await expect(page.locator('[data-testid="result"]')).toContainText('Expected text');
});
/**
* Test: [Another specific behavior]
*/
test('should [another expected behavior]', async ({ page }) => {
// Arrange
// Act
// Assert
});
});
/**
* Best Practices Checklist:
* ✅ Use only data-testid locators
* ✅ Follow AAA pattern (Arrange-Act-Assert)
* ✅ Use descriptive test names starting with "should"
* ✅ Add comments for complex logic
* ✅ Use explicit waits (waitForSelector, waitForLoadState)
* ✅ No hardcoded waits (waitForTimeout)
* ✅ One scenario per test
* ✅ Tests are independent and isolated
* ✅ Use TypeScript types
* ✅ Add meaningful assertions
*/

View File

@@ -0,0 +1,325 @@
import { Page, Locator, expect } from '@playwright/test';
/**
* Utility Functions for Playwright Tests
*
* Common helpers to reduce boilerplate and improve test readability.
* All functions follow best practices: data-testid locators, explicit waits,
* and proper error handling.
*/
/**
* Fill a form with multiple fields at once
*
* @param page - Playwright page object
* @param fields - Object mapping data-testid to values
*
* @example
* await fillForm(page, {
* 'username-input': 'testuser',
* 'password-input': 'password123',
* 'email-input': 'test@example.com'
* });
*/
export async function fillForm(
page: Page,
fields: Record<string, string>
): Promise<void> {
for (const [testId, value] of Object.entries(fields)) {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'visible' });
await locator.fill(value);
}
}
/**
* Click and wait for navigation
*
* @param page - Playwright page object
* @param testId - data-testid of the element to click
* @param expectedUrl - Optional URL pattern to wait for
*
* @example
* await clickAndNavigate(page, 'submit-button', '/dashboard');
*/
export async function clickAndNavigate(
page: Page,
testId: string,
expectedUrl?: string | RegExp
): Promise<void> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'visible' });
await Promise.all([
expectedUrl ? page.waitForURL(expectedUrl) : page.waitForLoadState('networkidle'),
locator.click(),
]);
}
/**
* Wait for element and verify visibility
*
* @param page - Playwright page object
* @param testId - data-testid of the element
* @param timeout - Optional timeout in milliseconds
*
* @example
* await waitForElement(page, 'success-message');
*/
export async function waitForElement(
page: Page,
testId: string,
timeout = 10000
): Promise<Locator> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'visible', timeout });
return locator;
}
/**
* Select option from dropdown
*
* @param page - Playwright page object
* @param testId - data-testid of the select element
* @param value - Value to select
*
* @example
* await selectOption(page, 'country-select', 'USA');
*/
export async function selectOption(
page: Page,
testId: string,
value: string
): Promise<void> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'visible' });
await locator.selectOption(value);
}
/**
* Check if element exists without throwing error
*
* @param page - Playwright page object
* @param testId - data-testid of the element
* @returns true if element exists, false otherwise
*
* @example
* if (await elementExists(page, 'error-message')) {
* // Handle error state
* }
*/
export async function elementExists(
page: Page,
testId: string
): Promise<boolean> {
try {
await page.locator(`[data-testid="${testId}"]`).waitFor({
state: 'visible',
timeout: 2000,
});
return true;
} catch {
return false;
}
}
/**
* Wait for element to disappear
*
* @param page - Playwright page object
* @param testId - data-testid of the element
* @param timeout - Optional timeout in milliseconds
*
* @example
* await waitForElementToDisappear(page, 'loading-spinner');
*/
export async function waitForElementToDisappear(
page: Page,
testId: string,
timeout = 10000
): Promise<void> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'hidden', timeout });
}
/**
* Get text content of an element
*
* @param page - Playwright page object
* @param testId - data-testid of the element
* @returns Text content of the element
*
* @example
* const username = await getTextContent(page, 'username-display');
*/
export async function getTextContent(
page: Page,
testId: string
): Promise<string> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'visible' });
const text = await locator.textContent();
return text?.trim() || '';
}
/**
* Upload file to input
*
* @param page - Playwright page object
* @param testId - data-testid of the file input
* @param filePath - Path to the file to upload
*
* @example
* await uploadFile(page, 'file-input', './test-data/sample.pdf');
*/
export async function uploadFile(
page: Page,
testId: string,
filePath: string
): Promise<void> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'attached' });
await locator.setInputFiles(filePath);
}
/**
* Check or uncheck a checkbox
*
* @param page - Playwright page object
* @param testId - data-testid of the checkbox
* @param checked - Whether to check or uncheck
*
* @example
* await toggleCheckbox(page, 'terms-checkbox', true);
*/
export async function toggleCheckbox(
page: Page,
testId: string,
checked: boolean
): Promise<void> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.waitFor({ state: 'visible' });
const isChecked = await locator.isChecked();
if (isChecked !== checked) {
await locator.click();
}
}
/**
* Retry an action with exponential backoff
*
* @param action - Async function to retry
* @param maxRetries - Maximum number of retries
* @param initialDelay - Initial delay in milliseconds
*
* @example
* await retryWithBackoff(async () => {
* await page.locator('[data-testid="submit"]').click();
* }, 3, 1000);
*/
export async function retryWithBackoff<T>(
action: () => Promise<T>,
maxRetries = 3,
initialDelay = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < maxRetries; i++) {
try {
return await action();
} catch (error) {
lastError = error as Error;
if (i < maxRetries - 1) {
const delay = initialDelay * Math.pow(2, i);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
/**
* Wait for API response
*
* @param page - Playwright page object
* @param urlPattern - URL pattern to wait for
* @param action - Action that triggers the API call
* @returns Response data
*
* @example
* const response = await waitForApiResponse(
* page,
* '**/api/users',
* () => page.locator('[data-testid="load-users"]').click()
* );
*/
export async function waitForApiResponse<T = any>(
page: Page,
urlPattern: string | RegExp,
action: () => Promise<void>
): Promise<T> {
const [response] = await Promise.all([
page.waitForResponse(urlPattern),
action(),
]);
return await response.json();
}
/**
* Take screenshot with timestamp
*
* @param page - Playwright page object
* @param name - Base name for the screenshot
*
* @example
* await takeTimestampedScreenshot(page, 'error-state');
* // Saves as: error-state-2024-01-15T10-30-45.png
*/
export async function takeTimestampedScreenshot(
page: Page,
name: string
): Promise<void> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
await page.screenshot({ path: `${name}-${timestamp}.png`, fullPage: true });
}
/**
* Assert multiple elements are visible
*
* @param page - Playwright page object
* @param testIds - Array of data-testid values
*
* @example
* await assertElementsVisible(page, ['header', 'nav', 'footer']);
*/
export async function assertElementsVisible(
page: Page,
testIds: string[]
): Promise<void> {
for (const testId of testIds) {
const locator = page.locator(`[data-testid="${testId}"]`);
await expect(locator).toBeVisible();
}
}
/**
* Get all text contents from multiple elements
*
* @param page - Playwright page object
* @param testId - data-testid of elements (can match multiple)
* @returns Array of text contents
*
* @example
* const productNames = await getAllTextContents(page, 'product-name');
*/
export async function getAllTextContents(
page: Page,
testId: string
): Promise<string[]> {
const locator = page.locator(`[data-testid="${testId}"]`);
await locator.first().waitFor({ state: 'visible' });
return await locator.allTextContents();
}

View File

@@ -0,0 +1,541 @@
# Test Maintainer Skill
## Purpose
Maintain, refactor, and improve existing Playwright E2E tests. Handles tasks like updating locators across test suites, extracting reusable utilities, improving test stability, removing code duplication, and enforcing best practices throughout the test codebase.
## When to Use This Skill
Use this skill when you need to:
- Update data-testid locators across multiple tests
- Refactor duplicate code into utilities or Page Objects
- Improve flaky or unstable tests
- Extract common test patterns into reusable fixtures
- Update tests after UI changes
- Migrate tests to use Page Object Model
- Consolidate similar tests
- Improve test readability and maintainability
Do NOT use this skill when:
- Creating new tests from scratch (use test-generator skill)
- Building new Page Objects (use page-object-builder skill)
- Debugging test failures (use test-debugger skill)
## Prerequisites
Before using this skill:
1. Access to existing test files
2. Understanding of what changes are needed
3. Knowledge of the current test structure
4. Optional: Test execution results to identify flaky tests
## Instructions
### Step 1: Assess Current State
Gather information about:
- **Test files** requiring maintenance
- **Type of maintenance** needed (refactor, update locators, fix flakiness)
- **Scope** of changes (single file, multiple files, entire suite)
- **Current issues** (duplication, poor practices, flakiness)
- **Desired end state** (what should the tests look like after)
### Step 2: Identify Maintenance Type
Determine the maintenance task:
**Locator Updates:**
- Changing data-testid values
- Updating selectors after UI changes
- Migrating from CSS/XPath to data-testid
**Code Refactoring:**
- Extracting duplicate code to utilities
- Creating Page Objects from inline selectors
- Consolidating similar tests
- Improving test structure
**Stability Improvements:**
- Adding explicit waits
- Fixing race conditions
- Removing hardcoded waits
- Improving assertions
**Best Practices:**
- Enforcing data-testid usage
- Implementing AAA pattern
- Adding proper TypeScript types
- Improving test isolation
### Step 3: Plan the Changes
Before making changes:
1. **Identify all affected files**
2. **Backup or commit current state** (git commit)
3. **Create checklist** of changes to make
4. **Plan refactoring strategy** (bottom-up or top-down)
5. **Consider impact** on other tests
### Step 4: Apply Maintenance
Execute the maintenance based on type:
#### Locator Updates
```typescript
// Task: Update data-testid from "btn-submit" to "submit-button"
// Before (multiple files)
await page.locator('[data-testid="btn-submit"]').click();
// After (updated in all files)
await page.locator('[data-testid="submit-button"]').click();
// Use search and replace across files
// Find: '[data-testid="btn-submit"]'
// Replace: '[data-testid="submit-button"]'
```
#### Extract Utilities
```typescript
// Before: Duplicate login code in multiple tests
test('test 1', async ({ page }) => {
await page.goto('/login');
await page.locator('[data-testid="email"]').fill('user@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login-button"]').click();
await page.waitForURL('/dashboard');
// ... test continues
});
// After: Extract to utility function
// In utils/auth.ts
export async function login(page: Page, email: string, password: string) {
await page.goto('/login');
await page.locator('[data-testid="email"]').fill(email);
await page.locator('[data-testid="password"]').fill(password);
await page.locator('[data-testid="login-button"]').click();
await page.waitForURL('/dashboard');
}
// In tests
test('test 1', async ({ page }) => {
await login(page, 'user@example.com', 'password');
// ... test continues
});
```
#### Migrate to Page Objects
```typescript
// Before: Inline selectors throughout tests
test('update profile', async ({ page }) => {
await page.goto('/profile');
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="save-button"]').click();
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});
// After: Using Page Object
// Create ProfilePage.ts (see page-object-builder skill)
test('update profile', async ({ page }) => {
const profilePage = new ProfilePage(page);
await profilePage.goto();
await profilePage.updateProfile({
name: 'John Doe',
email: 'john@example.com'
});
await expect(profilePage.getSuccessMessage()).toBeVisible();
});
```
#### Fix Flaky Tests
```typescript
// Before: Flaky due to race condition
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="result"]')).toContainText('Success');
// After: Add proper waits
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="result"]')).toContainText('Success', {
timeout: 10000
});
```
### Step 5: Ensure Consistency
After changes:
- [ ] All tests use data-testid locators
- [ ] Consistent naming conventions
- [ ] Follow AAA pattern
- [ ] Proper TypeScript types
- [ ] No code duplication
- [ ] Tests are isolated
- [ ] Proper waits (no hardcoded timeouts)
### Step 6: Verify Changes
Run tests to ensure:
1. **All tests pass** after refactoring
2. **No regressions** introduced
3. **Improved stability** (run multiple times)
4. **Better readability** and maintainability
5. **Reduced code duplication**
## Examples
### Example 1: Update Locators Across Test Suite
**Input:**
"The development team changed all button data-testids from format 'btn-action' to 'action-button'. Update all tests."
**Changes:**
```typescript
// Create mapping of old to new testids
const locatorUpdates = {
'btn-submit': 'submit-button',
'btn-cancel': 'cancel-button',
'btn-delete': 'delete-button',
'btn-edit': 'edit-button',
'btn-save': 'save-button',
};
// Apply to all test files:
// Find all instances in: tests/**/*.spec.ts
// Example in login.spec.ts:
// Before
await page.locator('[data-testid="btn-submit"]').click();
// After
await page.locator('[data-testid="submit-button"]').click();
// Use global find and replace for each mapping
```
**Verification:**
```bash
# Search for old pattern to ensure all updated
grep -r "btn-" tests/
# Run all tests
npx playwright test
# Check for any failures
```
### Example 2: Extract Common Test Utilities
**Input:**
"Multiple tests have duplicate code for filling forms. Extract to reusable utilities."
**Solution:**
```typescript
// Identify duplicate pattern across tests:
// Pattern 1: Form filling
await page.locator('[data-testid="field1"]').fill(value1);
await page.locator('[data-testid="field2"]').fill(value2);
await page.locator('[data-testid="field3"]').fill(value3);
// Create utils/form-helpers.ts:
import { Page } from '@playwright/test';
export async function fillForm(
page: Page,
fields: Record<string, string>
): Promise<void> {
for (const [testId, value] of Object.entries(fields)) {
await page.locator(`[data-testid="${testId}"]`).fill(value);
}
}
export async function submitForm(page: Page, submitButtonTestId: string): Promise<void> {
await page.locator(`[data-testid="${submitButtonTestId}"]`).waitFor({ state: 'visible' });
await page.locator(`[data-testid="${submitButtonTestId}"]`).click();
}
// Update all tests to use utilities:
import { fillForm, submitForm } from '../utils/form-helpers';
test('contact form submission', async ({ page }) => {
await page.goto('/contact');
await fillForm(page, {
'name-input': 'John Doe',
'email-input': 'john@example.com',
'message-input': 'Hello!',
});
await submitForm(page, 'submit-button');
await expect(page.locator('[data-testid="success"]')).toBeVisible();
});
```
### Example 3: Consolidate Similar Tests
**Input:**
"We have 5 tests that test form validation with different invalid inputs. Consolidate using test.each."
**Before:**
```typescript
test('should show error for empty email', async ({ page }) => {
await page.goto('/register');
await page.locator('[data-testid="email"]').fill('');
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
});
test('should show error for invalid email', async ({ page }) => {
await page.goto('/register');
await page.locator('[data-testid="email"]').fill('invalid');
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
});
// ... 3 more similar tests
```
**After:**
```typescript
const invalidEmails = [
{ email: '', description: 'empty email' },
{ email: 'invalid', description: 'invalid format' },
{ email: '@example.com', description: 'missing local part' },
{ email: 'user@', description: 'missing domain' },
{ email: 'user @example.com', description: 'space in email' },
];
test.describe('Email validation', () => {
for (const { email, description } of invalidEmails) {
test(`should show error for ${description}`, async ({ page }) => {
await page.goto('/register');
await page.locator('[data-testid="email"]').fill(email);
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
});
}
});
```
### Example 4: Improve Flaky Test
**Input:**
"Test 'user dashboard loads' fails intermittently with 'element not found' error."
**Analysis:**
```typescript
// Current test (flaky):
test('user dashboard loads', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toBeVisible();
await expect(page.locator('[data-testid="stats-card"]')).toHaveCount(4);
});
// Issue: Not waiting for data to load
```
**Solution:**
```typescript
test('user dashboard loads', async ({ page }) => {
await page.goto('/dashboard');
// Wait for page to fully load
await page.waitForLoadState('networkidle');
// Wait for API call to complete
await page.waitForResponse('**/api/dashboard');
// Now check elements
await expect(page.locator('[data-testid="welcome-message"]')).toBeVisible({
timeout: 10000
});
// Wait for all stats cards to load
await page.locator('[data-testid="stats-card"]').first().waitFor({ state: 'visible' });
await expect(page.locator('[data-testid="stats-card"]')).toHaveCount(4);
});
```
### Example 5: Migrate Test to Use Fixtures
**Input:**
"Tests require authentication but each test logs in manually. Create fixture for authenticated state."
**Solution:**
```typescript
// Create fixtures/auth.ts:
import { test as base, Page } from '@playwright/test';
type AuthFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// Login once
await page.goto('/login');
await page.locator('[data-testid="email"]').fill('test@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login-button"]').click();
await page.waitForURL('/dashboard');
await use(page);
// Cleanup if needed
},
});
export { expect } from '@playwright/test';
// Update tests:
// Before
import { test, expect } from '@playwright/test';
test('view profile', async ({ page }) => {
// Login code...
await page.goto('/login');
// ... more login code
// Actual test
await page.goto('/profile');
// ...
});
// After
import { test, expect } from '../fixtures/auth';
test('view profile', async ({ authenticatedPage: page }) => {
// Already logged in!
await page.goto('/profile');
// ... test continues
});
```
## Best Practices
### Refactoring Strategy
1. **Small incremental changes**: Refactor one thing at a time
2. **Run tests frequently**: After each change, verify tests still pass
3. **Use version control**: Commit after each successful refactoring
4. **Keep tests passing**: Never leave tests broken during refactoring
5. **Update related tests together**: Maintain consistency across suite
### Code Quality
1. **DRY principle**: Don't Repeat Yourself - extract common code
2. **Single Responsibility**: Each test tests one thing
3. **Clear naming**: Tests should describe what they verify
4. **Proper structure**: Follow AAA pattern consistently
5. **Type safety**: Use TypeScript types throughout
### Maintenance Patterns
1. **Centralize selectors**: Use Page Objects or constants
2. **Extract utilities**: Common actions go in helper functions
3. **Use fixtures**: Shared setup goes in fixtures
4. **Consistent waits**: Standardize waiting strategies
5. **Error handling**: Consistent approach to expected errors
## Common Issues and Solutions
### Issue 1: Large-Scale Locator Changes
**Problem:** Need to update hundreds of locators across many files
**Solutions:**
- Use IDE find and replace with regex
- Create a migration script
- Update and test incrementally (file by file)
- Use git to track changes and rollback if needed
```bash
# Example: Update all instances in all test files
find tests -name "*.spec.ts" -exec sed -i 's/btn-submit/submit-button/g' {} +
# Verify changes
git diff
# Run tests
npx playwright test
```
### Issue 2: Breaking Tests During Refactoring
**Problem:** Tests fail after refactoring
**Solutions:**
- Refactor smaller sections at a time
- Keep one version working while refactoring
- Use feature flags for gradual migration
- Maintain backward compatibility during transition
### Issue 3: Inconsistent Patterns Across Tests
**Problem:** Different tests use different approaches
**Solutions:**
- Document standard patterns in team guidelines
- Create templates for common test scenarios
- Use linting rules to enforce consistency
- Conduct code reviews to maintain standards
- Gradually migrate old tests to new patterns
### Issue 4: Difficult to Extract Common Code
**Problem:** Tests are similar but not identical
**Solutions:**
- Identify the varying parts and parameterize them
- Use fixtures with parameters
- Create flexible utility functions
- Consider builder pattern for complex setups
```typescript
// Flexible utility with options
async function performLogin(
page: Page,
options: {
email?: string;
password?: string;
rememberMe?: boolean;
expectSuccess?: boolean;
} = {}
) {
const {
email = 'default@example.com',
password = 'password',
rememberMe = false,
expectSuccess = true,
} = options;
await page.goto('/login');
await page.locator('[data-testid="email"]').fill(email);
await page.locator('[data-testid="password"]').fill(password);
if (rememberMe) {
await page.locator('[data-testid="remember-me"]').check();
}
await page.locator('[data-testid="login-button"]').click();
if (expectSuccess) {
await page.waitForURL('/dashboard');
}
}
```
### Issue 5: Tests Become Over-Abstracted
**Problem:** Too many layers of abstraction make tests hard to understand
**Solutions:**
- Balance DRY with readability
- Keep tests readable - it's OK to have some duplication
- Don't abstract everything - abstract common patterns
- Inline simple operations rather than creating tiny utilities
- Document complex abstractions
## Resources
The `resources/` directory contains helpful references:
- `refactoring-patterns.md` - Common refactoring patterns for tests
- `migration-guide.md` - Guide for migrating tests to new patterns
- `best-practices.md` - Testing best practices checklist

View File

@@ -0,0 +1,81 @@
# Playwright Testing Best Practices Checklist
## Test Structure
- [ ] Tests follow AAA pattern (Arrange-Act-Assert)
- [ ] One assertion per test (or closely related assertions)
- [ ] Tests are independent and can run in any order
- [ ] Clear, descriptive test names using "should" format
- [ ] Proper use of test.describe for grouping related tests
## Locators
- [ ] **ONLY** data-testid locators used (no CSS/XPath)
- [ ] data-testid values are semantic and descriptive
- [ ] No brittle selectors (class names, IDs, XPath)
- [ ] Locators are unique on the page
- [ ] Use .first() or .nth() for intentional multiple elements
## Waiting & Timing
- [ ] Explicit waits before interactions
- [ ] NO hardcoded waits (page.waitForTimeout())
- [ ] Use waitForLoadState() after navigation
- [ ] Wait for network requests when needed
- [ ] Proper timeouts for slow operations
## Code Organization
- [ ] No code duplication - extract to utilities/Page Objects
- [ ] Use Page Object Model for complex pages
- [ ] Common setup in fixtures
- [ ] Utilities for repeated actions
- [ ] Clear file and folder structure
## TypeScript
- [ ] All functions have proper types
- [ ] No `any` types (use specific types)
- [ ] Interfaces for complex data structures
- [ ] Async/await used correctly
- [ ] Proper error handling
## Test Isolation
- [ ] Tests don't depend on each other
- [ ] Clean state before each test
- [ ] Proper cleanup in afterEach/afterAll
- [ ] No shared mutable state
- [ ] Each test creates its own data
## Assertions
- [ ] Use appropriate matchers (toBeVisible, toContainText, etc.)
- [ ] Assertions have proper error messages
- [ ] Wait for conditions before asserting
- [ ] Check both positive and negative cases
- [ ] Use expect() consistently
## Configuration
- [ ] Proper timeout settings
- [ ] Retries enabled for flaky tests
- [ ] Screenshot on failure
- [ ] Trace on first retry
- [ ] Parallel execution configured
## Documentation
- [ ] Complex test logic has comments
- [ ] Page Objects are documented
- [ ] Utilities have JSDoc comments
- [ ] README explains test structure
- [ ] Known issues documented
## Maintenance
- [ ] Regular review of flaky tests
- [ ] Remove obsolete tests
- [ ] Update tests when UI changes
- [ ] Refactor duplicate code
- [ ] Keep dependencies updated

View File

@@ -0,0 +1,418 @@
# Test Refactoring Patterns
## Pattern 1: Extract Method
**When:** Duplicate code appears in multiple tests
**Before:**
```typescript
test('test 1', async ({ page }) => {
await page.locator('[data-testid="email"]').fill('user@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login"]').click();
await page.waitForURL('/dashboard');
// test continues...
});
test('test 2', async ({ page }) => {
await page.locator('[data-testid="email"]').fill('user@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login"]').click();
await page.waitForURL('/dashboard');
// different test logic...
});
```
**After:**
```typescript
// utils/auth.ts
export async function login(page: Page, email = 'user@example.com', password = 'password') {
await page.locator('[data-testid="email"]').fill(email);
await page.locator('[data-testid="password"]').fill(password);
await page.locator('[data-testid="login"]').click();
await page.waitForURL('/dashboard');
}
// In tests
test('test 1', async ({ page }) => {
await login(page);
// test continues...
});
test('test 2', async ({ page }) => {
await login(page);
// different test logic...
});
```
## Pattern 2: Extract to Fixture
**When:** Setup code needed for multiple tests
**Before:**
```typescript
test('test 1', async ({ page }) => {
await page.goto('/login');
await page.locator('[data-testid="email"]').fill('test@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login"]').click();
await page.waitForURL('/dashboard');
// test logic...
});
```
**After:**
```typescript
// fixtures/auth.ts
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.locator('[data-testid="email"]').fill('test@example.com');
await page.locator('[data-testid="password"]').fill('password');
await page.locator('[data-testid="login"]').click();
await page.waitForURL('/dashboard');
await use(page);
},
});
// In tests
test('test 1', async ({ authenticatedPage: page }) => {
// Already logged in
// test logic...
});
```
## Pattern 3: Extract to Page Object
**When:** Many tests interact with the same page
**Before:**
```typescript
test('test 1', async ({ page }) => {
await page.goto('/profile');
await page.locator('[data-testid="name"]').fill('John');
await page.locator('[data-testid="save"]').click();
await expect(page.locator('[data-testid="success"]')).toBeVisible();
});
test('test 2', async ({ page }) => {
await page.goto('/profile');
await page.locator('[data-testid="email"]').fill('john@example.com');
await page.locator('[data-testid="save"]').click();
await expect(page.locator('[data-testid="success"]')).toBeVisible();
});
```
**After:**
```typescript
// page-objects/ProfilePage.ts
export class ProfilePage {
constructor(readonly page: Page) {}
async goto() {
await this.page.goto('/profile');
}
async updateName(name: string) {
await this.page.locator('[data-testid="name"]').fill(name);
}
async updateEmail(email: string) {
await this.page.locator('[data-testid="email"]').fill(email);
}
async save() {
await this.page.locator('[data-testid="save"]').click();
}
getSuccessMessage() {
return this.page.locator('[data-testid="success"]');
}
}
// In tests
test('test 1', async ({ page }) => {
const profilePage = new ProfilePage(page);
await profilePage.goto();
await profilePage.updateName('John');
await profilePage.save();
await expect(profilePage.getSuccessMessage()).toBeVisible();
});
```
## Pattern 4: Parameterized Tests
**When:** Same test logic with different inputs
**Before:**
```typescript
test('validates email 1', async ({ page }) => {
await page.locator('[data-testid="email"]').fill('invalid');
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="error"]')).toBeVisible();
});
test('validates email 2', async ({ page }) => {
await page.locator('[data-testid="email"]').fill('@example.com');
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="error"]')).toBeVisible();
});
```
**After:**
```typescript
const invalidEmails = [
'invalid',
'@example.com',
'user@',
'',
'user @example.com',
];
for (const email of invalidEmails) {
test(`validates email: ${email}`, async ({ page }) => {
await page.locator('[data-testid="email"]').fill(email);
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="error"]')).toBeVisible();
});
}
```
## Pattern 5: Builder Pattern for Complex Setup
**When:** Tests need complex object creation
**Before:**
```typescript
test('create user', async ({ page }) => {
await page.locator('[data-testid="name"]').fill('John');
await page.locator('[data-testid="email"]').fill('john@example.com');
await page.locator('[data-testid="age"]').fill('30');
await page.locator('[data-testid="country"]').selectOption('USA');
await page.locator('[data-testid="bio"]').fill('Software engineer');
// ... many more fields
});
```
**After:**
```typescript
// utils/UserBuilder.ts
export class UserBuilder {
private data: Record<string, string> = {
name: 'Default Name',
email: 'default@example.com',
age: '25',
country: 'USA',
};
withName(name: string) {
this.data.name = name;
return this;
}
withEmail(email: string) {
this.data.email = email;
return this;
}
async fillForm(page: Page) {
for (const [field, value] of Object.entries(this.data)) {
await page.locator(`[data-testid="${field}"]`).fill(value);
}
}
}
// In test
test('create user', async ({ page }) => {
await new UserBuilder()
.withName('John')
.withEmail('john@example.com')
.fillForm(page);
});
```
## Pattern 6: Test Data Factory
**When:** Need consistent test data across tests
**Before:**
```typescript
test('test 1', async ({ page }) => {
const user = {
email: 'test@example.com',
password: 'password123',
name: 'Test User',
};
// use user...
});
test('test 2', async ({ page }) => {
const user = {
email: 'test2@example.com',
password: 'password123',
name: 'Test User 2',
};
// use user...
});
```
**After:**
```typescript
// utils/factories.ts
export const createUser = (overrides = {}) => ({
email: 'test@example.com',
password: 'password123',
name: 'Test User',
...overrides,
});
// In tests
test('test 1', async ({ page }) => {
const user = createUser();
// use user...
});
test('test 2', async ({ page }) => {
const user = createUser({ email: 'test2@example.com', name: 'Test User 2' });
// use user...
});
```
## Pattern 7: Extract Wait Strategy
**When:** Consistent waiting needed across tests
**Before:**
```typescript
test('test 1', async ({ page }) => {
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="result"]')).toBeVisible({ timeout: 10000 });
});
test('test 2', async ({ page }) => {
await page.locator('[data-testid="save"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="success"]')).toBeVisible({ timeout: 10000 });
});
```
**After:**
```typescript
// utils/wait-helpers.ts
export async function waitForActionComplete(page: Page, resultTestId: string) {
await page.waitForLoadState('networkidle');
await page.locator(`[data-testid="${resultTestId}"]`).waitFor({
state: 'visible',
timeout: 10000,
});
}
// In tests
test('test 1', async ({ page }) => {
await page.locator('[data-testid="submit"]').click();
await waitForActionComplete(page, 'result');
});
```
## Pattern 8: Consolidate Assertions
**When:** Similar assertions repeated across tests
**Before:**
```typescript
test('test 1', async ({ page }) => {
// ... actions
await expect(page.locator('[data-testid="success"]')).toBeVisible();
await expect(page.locator('[data-testid="success"]')).toContainText('Success');
await expect(page.locator('[data-testid="error"]')).not.toBeVisible();
});
```
**After:**
```typescript
// utils/assertions.ts
export async function assertSuccess(page: Page) {
await expect(page.locator('[data-testid="success"]')).toBeVisible();
await expect(page.locator('[data-testid="success"]')).toContainText('Success');
await expect(page.locator('[data-testid="error"]')).not.toBeVisible();
}
// In tests
test('test 1', async ({ page }) => {
// ... actions
await assertSuccess(page);
});
```
## Pattern 9: Extract Navigation Logic
**When:** Complex navigation patterns repeated
**Before:**
```typescript
test('test 1', async ({ page }) => {
await page.goto('/');
await page.locator('[data-testid="menu"]').click();
await page.locator('[data-testid="settings"]').click();
await page.locator('[data-testid="profile"]').click();
// test logic...
});
```
**After:**
```typescript
// utils/navigation.ts
export async function navigateToProfile(page: Page) {
await page.goto('/');
await page.locator('[data-testid="menu"]').click();
await page.locator('[data-testid="settings"]').click();
await page.locator('[data-testid="profile"]').click();
await page.waitForLoadState('domcontentloaded');
}
// In tests
test('test 1', async ({ page }) => {
await navigateToProfile(page);
// test logic...
});
```
## Pattern 10: Replace Magic Strings with Constants
**When:** Same strings/values used in multiple places
**Before:**
```typescript
test('test 1', async ({ page }) => {
await page.locator('[data-testid="submit-button"]').click();
});
test('test 2', async ({ page }) => {
await expect(page.locator('[data-testid="submit-button"]')).toBeEnabled();
});
```
**After:**
```typescript
// constants/testids.ts
export const TESTIDS = {
SUBMIT_BUTTON: 'submit-button',
CANCEL_BUTTON: 'cancel-button',
// ... more testids
};
// In tests
test('test 1', async ({ page }) => {
await page.locator(`[data-testid="${TESTIDS.SUBMIT_BUTTON}"]`).click();
});
// Or create a helper
function getByTestId(page: Page, testId: string) {
return page.locator(`[data-testid="${testId}"]`);
}
test('test 1', async ({ page }) => {
await getByTestId(page, TESTIDS.SUBMIT_BUTTON).click();
});
```