Initial commit
This commit is contained in:
377
skills/test-generator/SKILL.md
Normal file
377
skills/test-generator/SKILL.md
Normal 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
|
||||
131
skills/test-generator/resources/fixtures.ts
Normal file
131
skills/test-generator/resources/fixtures.ts
Normal 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);
|
||||
* }
|
||||
*/
|
||||
106
skills/test-generator/resources/playwright.config.ts
Normal file
106
skills/test-generator/resources/playwright.config.ts
Normal 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,
|
||||
// },
|
||||
});
|
||||
76
skills/test-generator/resources/test-template.ts
Normal file
76
skills/test-generator/resources/test-template.ts
Normal 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
|
||||
*/
|
||||
325
skills/test-generator/resources/utils.ts
Normal file
325
skills/test-generator/resources/utils.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user