| name | Playwright E2E Testing |
| description | Build end-to-end tests with Playwright, Feature Object pattern, cross-browser testing, and visual regression. Apply when testing critical user flows, automating regression testing, or validating integrations. |
| allowed-tools | Read, Write, Edit, Bash |
| version | 1.0.0 |
Playwright E2E Testing
Systematic end-to-end testing with Playwright ensuring critical user flows work correctly.
Overview
This Skill enforces:
- Feature Object pattern (replaces Page Object)
- Arrange-Act-Assert test structure
- Cross-browser testing (Chromium, Firefox, WebKit)
- Reliable selectors (getByRole, getByTestId)
- Visual regression testing
- Network mocking and interception
- Parallel test execution
- CI/CD integration
Apply when testing critical user flows, automating regression testing, or validating integrations.
Setup Playwright
Install
npm install -D @playwright/test
npx playwright install
Configure playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
}
});
Package.json Scripts
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}
}
Feature Object Pattern
Directory Structure
e2e/
├── features/
│ ├── auth.feature.ts # Authentication flows
│ ├── users.feature.ts # User management
│ └── dashboard.feature.ts # Dashboard features
├── fixtures/
│ ├── api.fixture.ts # API interactions
│ └── ui.fixture.ts # UI interactions
└── tests/
├── auth.spec.ts
├── users.spec.ts
└── dashboard.spec.ts
Feature Object (Auth)
// e2e/features/auth.feature.ts
import { Page } from '@playwright/test';
export class AuthFeature {
constructor(private page: Page) {}
async navigateToLogin() {
await this.page.goto('/login');
}
async enterEmail(email: string) {
await this.page.getByLabel('Email').fill(email);
}
async enterPassword(password: string) {
await this.page.getByLabel('Password').fill(password);
}
async clickLoginButton() {
await this.page.getByRole('button', { name: /login/i }).click();
}
async verifyLoginSuccess() {
await this.page.waitForURL('/dashboard');
await expect(this.page).toHaveURL('/dashboard');
}
async verifyLoginError(message: string) {
const error = this.page.getByRole('alert');
await expect(error).toContainText(message);
}
async logout() {
await this.page.getByRole('button', { name: /logout/i }).click();
await this.page.waitForURL('/login');
}
}
Fixture Setup
// e2e/fixtures/test.fixture.ts
import { test as base } from '@playwright/test';
import { AuthFeature } from '../features/auth.feature';
import { DashboardFeature } from '../features/dashboard.feature';
type Fixtures = {
auth: AuthFeature;
dashboard: DashboardFeature;
};
export const test = base.extend<Fixtures>({
auth: async ({ page }, use) => {
const auth = new AuthFeature(page);
await use(auth);
},
dashboard: async ({ page }, use) => {
const dashboard = new DashboardFeature(page);
await use(dashboard);
}
});
export { expect } from '@playwright/test';
Arrange-Act-Assert Pattern
Login Test
// e2e/tests/auth.spec.ts
import { test, expect } from '../fixtures/test.fixture';
test.describe('Authentication', () => {
test('successful login flow', async ({ auth }) => {
// ARRANGE: Navigate to login page
await auth.navigateToLogin();
// ACT: Enter credentials and submit
await auth.enterEmail('user@example.com');
await auth.enterPassword('password123');
await auth.clickLoginButton();
// ASSERT: Verify login success
await auth.verifyLoginSuccess();
});
test('login with invalid credentials', async ({ auth }) => {
// ARRANGE
await auth.navigateToLogin();
// ACT
await auth.enterEmail('user@example.com');
await auth.enterPassword('wrongpassword');
await auth.clickLoginButton();
// ASSERT
await auth.verifyLoginError('Invalid credentials');
});
test('logout flow', async ({ page, auth }) => {
// ARRANGE: Login first
await auth.navigateToLogin();
await auth.enterEmail('user@example.com');
await auth.enterPassword('password123');
await auth.clickLoginButton();
await page.waitForURL('/dashboard');
// ACT
await auth.logout();
// ASSERT
await expect(page).toHaveURL('/login');
});
});
Selectors (getByRole, getByTestId)
Recommended Selectors
// ✅ GOOD: Accessible selectors
await page.getByRole('button', { name: /submit/i }).click();
await page.getByRole('textbox', { name: /email/i }).fill('user@example.com');
await page.getByLabel('Password').fill('password');
await page.getByText('Welcome, Alice').isVisible();
// ✅ GOOD: Test IDs (for complex elements)
<div data-testid="user-profile">Profile</div>
await page.getByTestId('user-profile').click();
// ❌ BAD: Fragile selectors (auto-generated)
await page.locator('.css-1a2b3c4d').click(); // Will break on CSS change
// ❌ BAD: XPath (brittle)
await page.locator('//*[@class="button"]').click();
// ❌ BAD: Overly specific
await page.locator('div > section > form > button').click();
Critical Admin Flows
User Management Test
// e2e/features/admin.feature.ts
export class AdminFeature {
constructor(private page: Page) {}
async navigateToUsers() {
await this.page.goto('/admin/users');
}
async createUser(user: { name: string; email: string; role: string }) {
await this.page.getByRole('button', { name: /new user/i }).click();
await this.page.getByLabel('Name').fill(user.name);
await this.page.getByLabel('Email').fill(user.email);
await this.page.getByLabel('Role').selectOption(user.role);
await this.page.getByRole('button', { name: /create/i }).click();
await this.page.waitForSelector('text=User created');
}
async deleteUser(email: string) {
// Find user row and click delete
const row = this.page.locator(`tr:has-text("${email}")`);
await row.getByRole('button', { name: /delete/i }).click();
// Confirmation dialog
await this.page.getByRole('button', { name: /confirm/i }).click();
await this.page.waitForSelector('text=User deleted');
}
async verifyUserExists(email: string) {
await expect(this.page.locator('table')).toContainText(email);
}
}
// e2e/tests/admin.spec.ts
test('admin creates and deletes user', async ({ page, admin }) => {
// ARRANGE
await admin.navigateToUsers();
// ACT
await admin.createUser({
name: 'John Doe',
email: 'john@example.com',
role: 'admin'
});
// ASSERT
await admin.verifyUserExists('john@example.com');
// ACT: Delete user
await admin.deleteUser('john@example.com');
// ASSERT: User gone
await expect(page.locator('table')).not.toContainText('john@example.com');
});
Network Mocking
Mock API Responses
test('handles API failure gracefully', async ({ page }) => {
// Mock API to return error
await page.route('/api/users', route => {
route.abort('failed');
});
await page.goto('/users');
// Verify error message shown
await expect(page.getByText('Failed to load users')).toBeVisible();
});
test('intercept and modify response', async ({ page }) => {
await page.route('/api/users', route => {
route.continue();
// Wait for response
const response = route.response();
if (response) {
const json = response.json();
json.then(data => {
// Response intercepted and logged
console.log('Users API Response:', data);
});
}
});
await page.goto('/users');
});
Visual Regression Testing
Screenshot Comparison
test('dashboard layout', async ({ page }) => {
await page.goto('/dashboard');
// Take screenshot
await expect(page).toHaveScreenshot('dashboard.png');
});
test('responsive design', async ({ browser }) => {
const contexts = [
{ name: 'desktop', width: 1920, height: 1080 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'mobile', width: 375, height: 812 }
];
for (const context of contexts) {
const page = await browser.newPage({
viewport: { width: context.width, height: context.height }
});
await page.goto('/dashboard');
await expect(page).toHaveScreenshot(`dashboard-${context.name}.png`);
await page.close();
}
});
CI/CD Integration
GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Anti-Patterns
// ❌ BAD: Hard-coded waits
await page.waitForTimeout(5000);
// ✅ GOOD: Wait for element
await page.getByRole('button').waitFor({ state: 'visible' });
// ❌ BAD: Vague test names
test('test login', async ({ auth }) => {});
// ✅ GOOD: Descriptive test names
test('successfully login with valid credentials', async ({ auth }) => {});
// ❌ BAD: No error handling
const button = page.locator('button');
await button.click();
// Button might not exist!
// ✅ GOOD: Check existence first
await expect(page.getByRole('button')).toBeVisible();
await page.getByRole('button').click();
// ❌ BAD: Fragile selectors
await page.locator('div:nth-child(3) > button').click();
// ✅ GOOD: Semantic selectors
await page.getByRole('button', { name: /submit/i }).click();
Running Tests
# Run all tests
npm run test:e2e
# Run specific file
npx playwright test auth.spec.ts
# Run in UI mode
npm run test:e2e:ui
# Debug mode
npm run test:e2e:debug
# Report
npx playwright show-report
Verification Before Production
- Critical user flows covered
- Cross-browser tests passing
- No hard-coded waits
- Reliable selectors used
- Visual regressions checked
- Network edge cases mocked
- Mobile testing included
- CI/CD integrated
- Tests run in parallel
- Reports generated
Integration with Project Standards
Enforces T-7, T-9:
- E2E tests for critical admin flows
- Admin portal features tested
- User journey validation
- Integration verification
Resources
- Playwright Docs: https://playwright.dev
- Selectors Guide: https://playwright.dev/docs/locators
- Best Practices: https://playwright.dev/docs/best-practices
- CI/CD: https://playwright.dev/docs/ci