| name | playwright |
| description | Playwright E2E testing patterns. Trigger: When writing Playwright E2E tests (Page Object Model, selectors, MCP exploration workflow).
|
| license | Apache-2.0 |
| metadata | {"author":"Mane087","version":"1.0","scope":["root","ui"],"auto_invoke":"Writing Playwright E2E tests"} |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task |
MCP Workflow (MANDATORY If Available)
⚠️ If you have Playwright MCP tools, ALWAYS use them BEFORE creating any test:
- Navigate to target page
- Take snapshot to see page structure and elements
- Interact with forms/elements to verify exact user flow
- Take screenshots to document expected states
- Verify page transitions through complete flow (loading, success, error)
- Document actual selectors from snapshots (use real refs and labels)
- Only after exploring create test code with verified selectors
If MCP NOT available: Proceed with test creation based on docs and code analysis.
Why This Matters:
- ✅ Precise tests - exact steps needed, no assumptions
- ✅ Accurate selectors - real DOM structure, not imagined
- ✅ Real flow validation - verify journey actually works
- ✅ Avoid over-engineering - minimal tests for what exists
- ✅ Prevent flaky tests - real exploration = stable tests
- ❌ Never assume how UI "should" work
File Structure
e2e/
├── app
├── components/
├── {file-name}.2e2.spec.ts
File Naming:
- ✅
modal.e2e.spec.ts (all sign-up tests)
- ❌
sign-up-critical-path.spec.ts (WRONG - no separate files)
- ❌
sign-up-validation.spec.ts (WRONG)
Selector Priority (REQUIRED)
this.submitButton = page.getByRole('button', { name: 'Submit' });
this.navLink = page.getByRole('link', { name: 'Dashboard' });
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.errorMessage = page.getByText('Invalid credentials');
this.pageTitle = page.getByText('Welcome');
this.customWidget = page.getByTestId('date-picker');
this.button = page.locator('.btn-primary');
this.input = page.locator('#email');
Scope Detection (ASK IF AMBIGUOUS)
| User Says | Action |
|---|
| "a test", "one test", "new test", "add test" | Create ONE test() in existing spec |
| "comprehensive tests", "all tests", "test suite", "generate tests" | Create full suite |
Examples:
- "Create a test for user sign-up" → ONE test only
- "Generate E2E tests for login page" → Full suite
- "Add a test to verify form validation" → ONE test to existing spec
Page Object Pattern
import { Page, Locator, expect } from '@playwright/test';
export class BasePage {
constructor(protected page: Page) {}
async goto(path: string): Promise<void> {
await this.page.goto(path);
await this.page.waitForLoadState('networkidle');
}
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
async verifyNotificationMessage(message: string): Promise<void> {
const notification = this.page.locator('[role="status"]');
await expect(notification).toContainText(message);
}
}
export interface LoginData {
email: string;
password: string;
}
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
async goto(): Promise<void> {
await super.goto('/login');
}
async login(data: LoginData): Promise<void> {
await this.emailInput.fill(data.email);
await this.passwordInput.fill(data.password);
await this.submitButton.click();
}
async verifyCriticalOutcome(): Promise<void> {
await expect(this.page).toHaveURL('/dashboard');
}
}
Page Object Reuse (CRITICAL)
Always check existing page objects before creating new ones!
import { SignInPage } from '../sign-in/sign-in-page';
import { HomePage } from '../home/home-page';
test('User can sign up and login', async ({ page }) => {
const signUpPage = new SignUpPage(page);
const signInPage = new SignInPage(page);
const homePage = new HomePage(page);
await signUpPage.signUp(userData);
await homePage.verifyPageLoaded();
await homePage.signOut();
await signInPage.login(credentials);
});
export class SignUpPage extends BasePage {
async logout() {
}
async login() {
}
}
Guidelines:
- Check
tests/ for existing page objects first
- Import and reuse existing pages
- Create page objects only when page doesn't exist
- If test requires multiple pages, ensure all page objects exist (create if needed)
Refactoring Guidelines
Move to BasePage when:
- ✅ Navigation helpers used by multiple pages (
waitForPageLoad(), getCurrentUrl())
- ✅ Common UI interactions (notifications, modals, theme toggles)
- ✅ Verification patterns repeated across pages (
isVisible(), waitForVisible())
- ✅ Error handling that applies to all pages
- ✅ Screenshot utilities for debugging
Move to helpers.ts when:
- ✅ Test data generation (
generateUniqueEmail(), generateTestUser())
- ✅ Setup/teardown utilities (
createTestUser(), cleanupTestData())
- ✅ Custom assertions (
expectNotificationToContain())
- ✅ API helpers for test setup (
seedDatabase(), resetState())
- ✅ Time utilities (
waitForCondition(), retryAction())
Before (BAD):
export class SignUpPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
export class SignInPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
After (GOOD):
export class BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
export function generateUniqueEmail(): string {
return `test.${Date.now()}@example.com`;
}
export function generateTestUser() {
return {
name: 'Test User',
email: generateUniqueEmail(),
password: 'TestPassword123!',
};
}
Test Pattern with Tags
import { test, expect } from '@playwright/test';
import { LoginPage } from './login-page';
test.describe('Login', () => {
test(
'User can login successfully',
{ tag: ['@critical', '@e2e', '@login', '@LOGIN-E2E-001'] },
async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login({ email: 'user@test.com', password: 'pass123' });
await expect(page).toHaveURL('/dashboard');
},
);
});
Tag Categories:
- Priority:
@critical, @high, @medium, @low
- Type:
@e2e
- Feature:
@signup, @signin, @dashboard
- Test ID:
@SIGNUP-E2E-001, @LOGIN-E2E-002
Test Documentation Format ({page-name}.md)
### E2E Tests: {Feature Name}
**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}
---
## Test Case: `{TEST-ID}` - {Test case title}
**Priority:** `{critical|high|medium|low}`
**Tags:**
- type → @e2e
- feature → @{feature-name}
**Description/Objective:** {Brief description}
**Preconditions:**
- {Prerequisites for test to run}
- {Required data or state}
### Flow Steps:
1. {Step 1}
2. {Step 2}
3. {Step 3}
### Expected Result:
- {Expected outcome 1}
- {Expected outcome 2}
### Key verification points:
- {Assertion 1}
- {Assertion 2}
### Notes:
- {Additional considerations}
Documentation Rules:
- ❌ NO general test running instructions
- ❌ NO file structure explanations
- ❌ NO code examples or tutorials
- ❌ NO troubleshooting sections
- ✅ Focus ONLY on specific test case
- ✅ Keep under 60 lines when possible