From bc292955bbf72f1ab0ce3c6dc2bbe02a2496c943 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:28:25 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 14 + README.md | 3 + commands/create-page-object.md | 72 ++ commands/debug-test.md | 80 ++ commands/fix-flaky.md | 88 +++ commands/generate-test.md | 81 ++ plugin.lock.json | 121 +++ skills/page-object-builder/SKILL.md | 727 ++++++++++++++++++ .../resources/base-page.ts | 265 +++++++ .../resources/component-template.ts | 161 ++++ .../resources/page-template.ts | 112 +++ skills/test-debugger/SKILL.md | 498 ++++++++++++ .../test-debugger/resources/common-errors.md | 378 +++++++++ .../resources/debugging-checklist.md | 250 ++++++ .../resources/playwright-commands.md | 480 ++++++++++++ skills/test-generator/SKILL.md | 377 +++++++++ skills/test-generator/resources/fixtures.ts | 131 ++++ .../resources/playwright.config.ts | 106 +++ .../test-generator/resources/test-template.ts | 76 ++ skills/test-generator/resources/utils.ts | 325 ++++++++ skills/test-maintainer/SKILL.md | 541 +++++++++++++ .../resources/best-practices.md | 81 ++ .../resources/refactoring-patterns.md | 418 ++++++++++ 23 files changed, 5385 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/create-page-object.md create mode 100644 commands/debug-test.md create mode 100644 commands/fix-flaky.md create mode 100644 commands/generate-test.md create mode 100644 plugin.lock.json create mode 100644 skills/page-object-builder/SKILL.md create mode 100644 skills/page-object-builder/resources/base-page.ts create mode 100644 skills/page-object-builder/resources/component-template.ts create mode 100644 skills/page-object-builder/resources/page-template.ts create mode 100644 skills/test-debugger/SKILL.md create mode 100644 skills/test-debugger/resources/common-errors.md create mode 100644 skills/test-debugger/resources/debugging-checklist.md create mode 100644 skills/test-debugger/resources/playwright-commands.md create mode 100644 skills/test-generator/SKILL.md create mode 100644 skills/test-generator/resources/fixtures.ts create mode 100644 skills/test-generator/resources/playwright.config.ts create mode 100644 skills/test-generator/resources/test-template.ts create mode 100644 skills/test-generator/resources/utils.ts create mode 100644 skills/test-maintainer/SKILL.md create mode 100644 skills/test-maintainer/resources/best-practices.md create mode 100644 skills/test-maintainer/resources/refactoring-patterns.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..396facc --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9aecdb --- /dev/null +++ b/README.md @@ -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. diff --git a/commands/create-page-object.md b/commands/create-page-object.md new file mode 100644 index 0000000..e6edb9f --- /dev/null +++ b/commands/create-page-object.md @@ -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 diff --git a/commands/debug-test.md b/commands/debug-test.md new file mode 100644 index 0000000..7b00e62 --- /dev/null +++ b/commands/debug-test.md @@ -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 diff --git a/commands/fix-flaky.md b/commands/fix-flaky.md new file mode 100644 index 0000000..2b2c341 --- /dev/null +++ b/commands/fix-flaky.md @@ -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 diff --git a/commands/generate-test.md b/commands/generate-test.md new file mode 100644 index 0000000..9f7ef3c --- /dev/null +++ b/commands/generate-test.md @@ -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 diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..f02c189 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/skills/page-object-builder/SKILL.md b/skills/page-object-builder/SKILL.md new file mode 100644 index 0000000..97acb07 --- /dev/null +++ b/skills/page-object-builder/SKILL.md @@ -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 +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 { + // 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 { + return await this.element.textContent() || ''; +} + +async isElementVisible(): Promise { + 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 { + 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 { + 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 { + await this.usernameInput.fill(username); + } + + /** + * Fill only the password field + */ + async fillPassword(password: string): Promise { + await this.passwordInput.fill(password); + } + + /** + * Click the login button + */ + async clickLogin(): Promise { + await this.loginButton.click(); + } + + /** + * Click forgot password link + */ + async clickForgotPassword(): Promise { + 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 { + 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 { + 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 { + await this.page.goto(`/products/${productId}`); + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Set the quantity for purchase + */ + async setQuantity(quantity: number): Promise { + 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 { + if (quantity > 1) { + await this.setQuantity(quantity); + } + await this.addToCartButton.waitFor({ state: 'enabled' }); + await this.addToCartButton.click(); + } + + /** + * Click Buy Now button + */ + async buyNow(): Promise { + await this.buyNowButton.click(); + } + + /** + * Add product to wishlist + */ + async addToWishlist(): Promise { + await this.wishlistButton.click(); + } + + /** + * Click share button + */ + async shareProduct(): Promise { + await this.shareButton.click(); + } + + /** + * Get product name text + */ + async getProductName(): Promise { + const text = await this.productName.textContent(); + return text?.trim() || ''; + } + + /** + * Get product price text + */ + async getProductPrice(): Promise { + const text = await this.productPrice.textContent(); + return text?.trim() || ''; + } + + /** + * Get number of reviews + */ + async getReviewCount(): Promise { + return await this.reviewItems.count(); + } + + /** + * Get average rating text + */ + async getAverageRating(): Promise { + 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 { + await this.page.goto('/dashboard'); + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Navigate using sidebar + */ + async navigateToHome(): Promise { + await this.homeLink.click(); + } + + async navigateToProjects(): Promise { + await this.projectsLink.click(); + } + + async navigateToSettings(): Promise { + await this.settingsLink.click(); + } + + /** + * Search functionality + */ + async search(query: string): Promise { + await this.searchBar.fill(query); + await this.page.keyboard.press('Enter'); + } + + /** + * Profile dropdown actions + */ + async openProfileDropdown(): Promise { + await this.userProfileDropdown.click(); + await this.profileMenu.waitFor({ state: 'visible' }); + } + + async navigateToProfile(): Promise { + await this.openProfileDropdown(); + await this.profileLink.click(); + } + + async navigateToAccountSettings(): Promise { + await this.openProfileDropdown(); + await this.accountSettingsLink.click(); + } + + async logout(): Promise { + await this.logoutButton.click(); + } + + /** + * Get stats count + */ + async getStatsCount(): Promise { + 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, 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 diff --git a/skills/page-object-builder/resources/base-page.ts b/skills/page-object-builder/resources/base-page.ts new file mode 100644 index 0000000..d38a0c5 --- /dev/null +++ b/skills/page-object-builder/resources/base-page.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const locator = this.getByTestId(testId); + await locator.waitFor({ state: 'hidden', timeout }); + } + + /** + * Fill a form with multiple fields + */ + async fillForm(fields: Record): Promise { + 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 { + 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 { + const locator = this.getByTestId(testId); + await locator.waitFor({ state: 'visible' }); + await locator.selectOption(value); + } + + /** + * Check/uncheck checkbox + */ + async toggleCheckbox(testId: string, checked: boolean): Promise { + 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 { + 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 { + 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 { + return await this.getByTestId(testId).count(); + } + + /** + * Wait for API response + */ + async waitForApiResponse( + urlPattern: string | RegExp, + action: () => Promise + ): Promise { + 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 { + await this.page.reload(); + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Go back in browser history + */ + async goBack(): Promise { + await this.page.goBack(); + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Go forward in browser history + */ + async goForward(): Promise { + await this.page.goForward(); + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Take screenshot + */ + async takeScreenshot(name: string): Promise { + 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 { + * await super.goto('/login'); + * } + * + * async login(username: string, password: string): Promise { + * await this.fillForm({ + * 'username-input': username, + * 'password-input': password, + * }); + * await this.clickAndNavigate('login-button', '/dashboard'); + * } + * } + */ diff --git a/skills/page-object-builder/resources/component-template.ts b/skills/page-object-builder/resources/component-template.ts new file mode 100644 index 0000000..b8facf4 --- /dev/null +++ b/skills/page-object-builder/resources/component-template.ts @@ -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 { + await this.container.waitFor({ state: 'visible' }); + } + + /** + * Check if component is visible + */ + async isVisible(): Promise { + try { + await this.container.waitFor({ state: 'visible', timeout: 2000 }); + return true; + } catch { + return false; + } + } + + /** + * Component-specific action + */ + async performAction(): Promise { + 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 { + await this.modal.waitFor({ state: 'visible' }); + } + + async close(): Promise { + await this.closeButton.click(); + await this.modal.waitFor({ state: 'hidden' }); + } + + async confirm(): Promise { + await this.confirmButton.click(); + await this.modal.waitFor({ state: 'hidden' }); + } + + async cancel(): Promise { + await this.cancelButton.click(); + await this.modal.waitFor({ state: 'hidden' }); + } + + async getTitle(): Promise { + 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 { + await this.logo.click(); + } + + async clickMenuItem(name: string): Promise { + await this.menuItems.filter({ hasText: name }).click(); + } + + async openUserMenu(): Promise { + await this.userMenu.click(); + } + + async search(query: string): Promise { + await this.searchBar.fill(query); + await this.page.keyboard.press('Enter'); + } + + getNav(): Locator { + return this.nav; + } +} diff --git a/skills/page-object-builder/resources/page-template.ts b/skills/page-object-builder/resources/page-template.ts new file mode 100644 index 0000000..3e1891b --- /dev/null +++ b/skills/page-object-builder/resources/page-template.ts @@ -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 { + 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 { + 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 { + 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 { + const text = await this.elementName.textContent(); + return text?.trim() || ''; + } + + /** + * Check if element is visible + */ + async isElementVisible(): Promise { + 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] + */ diff --git a/skills/test-debugger/SKILL.md b/skills/test-debugger/SKILL.md new file mode 100644 index 0000000..d3b41b9 --- /dev/null +++ b/skills/test-debugger/SKILL.md @@ -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 diff --git a/skills/test-debugger/resources/common-errors.md b/skills/test-debugger/resources/common-errors.md new file mode 100644 index 0000000..3022a91 --- /dev/null +++ b/skills/test-debugger/resources/common-errors.md @@ -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: