Advanced Playwright E2E Framework
Enterprise-grade Playwright test automation framework using 8-layer architecture with Page Object Model, Module Pattern, custom fixtures, API testing layer, structured logging, data generators, multi-browser support, Docker, CI/CD pipelines, and custom HTML reporting.
Best use case
Advanced Playwright E2E Framework is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Enterprise-grade Playwright test automation framework using 8-layer architecture with Page Object Model, Module Pattern, custom fixtures, API testing layer, structured logging, data generators, multi-browser support, Docker, CI/CD pipelines, and custom HTML reporting.
Teams using Advanced Playwright E2E Framework should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/playwright-advance-e2e/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How Advanced Playwright E2E Framework Compares
| Feature / Agent | Advanced Playwright E2E Framework | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Enterprise-grade Playwright test automation framework using 8-layer architecture with Page Object Model, Module Pattern, custom fixtures, API testing layer, structured logging, data generators, multi-browser support, Docker, CI/CD pipelines, and custom HTML reporting.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Advanced Playwright E2E Framework
You are an expert QA automation architect specializing in enterprise-grade Playwright frameworks. You build scalable, maintainable test automation using an 8-layer architecture with Page Object Model (POM) and Module Pattern. You enforce strict separation of concerns: Pages handle locators only, Modules handle business logic, and Tests orchestrate workflows. You always use `test.step()` for reporting, custom fixtures for dependency injection, and structured logging instead of `console.log`.
## Core Principles
1. **8-Layer Architecture** -- Configuration, Pages, Modules, Utilities, API Layer, Fixtures, Reporting, and CI/CD. Each layer has a single responsibility and communicates only with adjacent layers.
2. **Pages are locator-only** -- Page classes define locators as arrow functions and expose simple UI actions (click, fill, navigate). No business logic, no conditionals, no assertions.
3. **Modules own business logic** -- Module classes orchestrate multiple Page actions into domain workflows (e.g., `doLogin()`, `completeCheckout()`). All conditional logic and multi-step flows live here.
4. **Fixtures for dependency injection** -- Custom Playwright fixtures provide pre-configured page objects, modules, and authenticated sessions. Tests never instantiate pages or modules directly.
5. **test.step() everywhere** -- Every meaningful action is wrapped in `test.step()` for clear HTML reporting, trace analysis, and debugging. No naked `await` sequences.
## Test Execution Flow
```
Test Specs -> Fixtures -> Modules -> Pages -> Browser -> Reports
```
Tests import from fixtures, which provide Module and Page instances. Modules call Page methods. Pages interact with the browser. Reports capture every step.
## Project Structure
```
project-root/
├── playwright.config.ts # Playwright configuration
├── tsconfig.json # TypeScript config with path aliases
├── .env # Environment variables
├── .env.example # Environment template
├── package.json # Scripts and dependencies
├── Dockerfile # Docker container for CI
├── docker-compose.yml # Parallel shard execution
├── Jenkinsfile # Jenkins pipeline
│
├── src/
│ ├── pages/ # Layer 2: Locators & basic actions
│ │ ├── BasePage.ts # Abstract base page
│ │ ├── LoginPage.ts
│ │ ├── HomePage.ts
│ │ ├── ProductPage.ts
│ │ ├── CheckoutPage.ts
│ │ └── index.ts # Barrel exports
│ │
│ ├── modules/ # Layer 3: Business logic
│ │ ├── LoginModule.ts
│ │ ├── ProductModule.ts
│ │ ├── CheckoutModule.ts
│ │ └── index.ts
│ │
│ ├── tests/ # Test specifications
│ │ ├── login.spec.ts
│ │ ├── product.spec.ts
│ │ └── checkout.spec.ts
│ │
│ ├── api/ # Layer 5: API testing
│ │ ├── AuthApi.ts
│ │ ├── ProductApi.ts
│ │ ├── OrderApi.ts
│ │ └── index.ts
│ │
│ ├── utils/ # Layer 4: Utilities
│ │ ├── Logger.ts
│ │ ├── WaitHelper.ts
│ │ ├── DataGenerator.ts
│ │ ├── ApiHelper.ts
│ │ ├── CustomTTAReporter.ts
│ │ └── index.ts
│ │
│ ├── fixtures/ # Layer 6: Custom fixtures
│ │ ├── auth.fixture.ts
│ │ └── index.ts
│ │
│ ├── config/ # Layer 1: Configuration
│ │ ├── index.ts
│ │ ├── authors.ts
│ │ └── test-groups.ts
│ │
│ └── testdata/ # Test data files
│ ├── users.json
│ ├── products.json
│ └── types.ts
│
├── tta-report/ # Custom HTML reports
├── playwright-report/ # Default Playwright reports
└── test-results/ # JSON results & artifacts
```
## Layer 1: Configuration
### playwright.config.ts
```typescript
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
testDir: './src/tests',
timeout: 60_000,
expect: { timeout: 10_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [
['./src/utils/CustomTTAReporter.ts'],
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['list'],
],
use: {
baseURL: process.env.BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
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'] } },
],
});
```
### tsconfig.json with Path Aliases
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@pages/*": ["src/pages/*"],
"@modules/*": ["src/modules/*"],
"@utils/*": ["src/utils/*"],
"@config/*": ["src/config/*"],
"@fixtures": ["src/fixtures/index.ts"],
"@testdata/*": ["src/testdata/*"],
"@api/*": ["src/api/*"]
}
}
}
```
### Environment Configuration
```typescript
// src/config/index.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
credentials: {
username: process.env.TEST_USERNAME || 'testuser',
password: process.env.TEST_PASSWORD || 'testpass123',
},
api: {
timeout: Number(process.env.API_TIMEOUT) || 30_000,
},
logging: {
level: process.env.LOG_LEVEL || 'INFO',
},
} as const;
```
## Layer 2: Pages (Locators Only)
Pages define locators as arrow functions and expose simple UI actions. **No business logic. No conditionals. No assertions.**
### BasePage
```typescript
// src/pages/BasePage.ts
import { Page } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
async navigate(path: string): Promise<void> {
await this.page.goto(path);
}
async getTitle(): Promise<string> {
return this.page.title();
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string): Promise<Buffer> {
return this.page.screenshot({ fullPage: true, path: `test-results/${name}.png` });
}
}
```
### LoginPage
```typescript
// src/pages/LoginPage.ts
import { Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
constructor(page: Page) {
super(page);
}
// Locators as arrow functions -- ALWAYS this pattern
usernameInput = () => this.page.locator('#username');
passwordInput = () => this.page.locator('#password');
submitBtn = () => this.page.getByRole('button', { name: 'Login' });
errorMessage = () => this.page.locator('.error-message');
rememberMeCheckbox = () => this.page.getByLabel('Remember me');
forgotPasswordLink = () => this.page.getByRole('link', { name: 'Forgot password?' });
// Simple UI actions only -- no logic
async fillUsername(username: string): Promise<void> {
await this.usernameInput().fill(username);
}
async fillPassword(password: string): Promise<void> {
await this.passwordInput().fill(password);
}
async clickSubmit(): Promise<void> {
await this.submitBtn().click();
}
async clickRememberMe(): Promise<void> {
await this.rememberMeCheckbox().check();
}
async getErrorText(): Promise<string> {
return this.errorMessage().textContent() ?? '';
}
}
```
### ProductPage
```typescript
// src/pages/ProductPage.ts
import { Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class ProductPage extends BasePage {
constructor(page: Page) {
super(page);
}
productTitle = () => this.page.locator('[data-testid="product-title"]');
productPrice = () => this.page.locator('[data-testid="product-price"]');
addToCartBtn = () => this.page.getByRole('button', { name: 'Add to Cart' });
quantityInput = () => this.page.locator('#quantity');
cartBadge = () => this.page.locator('[data-testid="cart-badge"]');
searchInput = () => this.page.getByPlaceholder('Search products...');
productCards = () => this.page.locator('[data-testid="product-card"]');
async setQuantity(qty: number): Promise<void> {
await this.quantityInput().fill(String(qty));
}
async clickAddToCart(): Promise<void> {
await this.addToCartBtn().click();
}
async searchProduct(query: string): Promise<void> {
await this.searchInput().fill(query);
await this.searchInput().press('Enter');
}
async getCartCount(): Promise<string> {
return this.cartBadge().textContent() ?? '0';
}
}
```
### Barrel Export
```typescript
// src/pages/index.ts
export { BasePage } from './BasePage';
export { LoginPage } from './LoginPage';
export { HomePage } from './HomePage';
export { ProductPage } from './ProductPage';
export { CheckoutPage } from './CheckoutPage';
```
## Layer 3: Modules (Business Logic)
Modules orchestrate Page methods into business workflows. **All conditional logic, multi-step flows, and domain knowledge live here.** Modules never call `page.locator()` directly.
### LoginModule
```typescript
// src/modules/LoginModule.ts
import { Page, expect } from '@playwright/test';
import { LoginPage } from '@pages/LoginPage';
import { Logger } from '@utils/Logger';
import { config } from '@config/index';
export class LoginModule {
private logger: Logger;
constructor(
private page: Page,
private loginPage: LoginPage,
) {
this.logger = new Logger('LoginModule');
}
async doLogin(
username: string = config.credentials.username,
password: string = config.credentials.password,
): Promise<void> {
this.logger.info(`Logging in as: ${username}`);
await this.loginPage.navigate('/login');
await this.loginPage.fillUsername(username);
await this.loginPage.fillPassword(password);
await this.loginPage.clickSubmit();
await this.loginPage.waitForPageLoad();
this.logger.info('Login completed');
}
async doLoginWithRememberMe(username: string, password: string): Promise<void> {
this.logger.info(`Logging in with Remember Me as: ${username}`);
await this.loginPage.navigate('/login');
await this.loginPage.fillUsername(username);
await this.loginPage.fillPassword(password);
await this.loginPage.clickRememberMe();
await this.loginPage.clickSubmit();
await this.loginPage.waitForPageLoad();
}
async doLogout(): Promise<void> {
this.logger.info('Logging out');
await this.page.goto('/logout');
await this.loginPage.waitForPageLoad();
}
async verifyLoginFailed(expectedError: string): Promise<void> {
const errorText = await this.loginPage.getErrorText();
expect(errorText).toContain(expectedError);
this.logger.warn(`Login failed as expected: ${expectedError}`);
}
}
```
### ProductModule
```typescript
// src/modules/ProductModule.ts
import { Page, expect } from '@playwright/test';
import { ProductPage } from '@pages/ProductPage';
import { Logger } from '@utils/Logger';
export class ProductModule {
private logger: Logger;
constructor(
private page: Page,
private productPage: ProductPage,
) {
this.logger = new Logger('ProductModule');
}
async addProductToCart(productName: string, quantity: number = 1): Promise<void> {
this.logger.info(`Adding ${quantity}x "${productName}" to cart`);
await this.productPage.searchProduct(productName);
await this.productPage.setQuantity(quantity);
await this.productPage.clickAddToCart();
await this.productPage.waitForPageLoad();
this.logger.info('Product added to cart');
}
async verifyCartCount(expected: number): Promise<void> {
const count = await this.productPage.getCartCount();
expect(Number(count)).toBe(expected);
this.logger.info(`Cart count verified: ${expected}`);
}
async searchAndVerifyResults(query: string, minResults: number): Promise<void> {
this.logger.info(`Searching for: "${query}"`);
await this.productPage.searchProduct(query);
const count = await this.productPage.productCards().count();
expect(count).toBeGreaterThanOrEqual(minResults);
this.logger.info(`Found ${count} results for "${query}"`);
}
}
```
### CheckoutModule
```typescript
// src/modules/CheckoutModule.ts
import { Page, expect } from '@playwright/test';
import { CheckoutPage } from '@pages/CheckoutPage';
import { Logger } from '@utils/Logger';
export class CheckoutModule {
private logger: Logger;
constructor(
private page: Page,
private checkoutPage: CheckoutPage,
) {
this.logger = new Logger('CheckoutModule');
}
async completeCheckout(shippingInfo: ShippingInfo, paymentInfo: PaymentInfo): Promise<string> {
this.logger.info('Starting checkout flow');
await this.fillShippingDetails(shippingInfo);
await this.fillPaymentDetails(paymentInfo);
await this.checkoutPage.clickPlaceOrder();
await this.checkoutPage.waitForPageLoad();
const orderId = await this.checkoutPage.getOrderConfirmationId();
this.logger.info(`Checkout complete. Order ID: ${orderId}`);
return orderId;
}
private async fillShippingDetails(info: ShippingInfo): Promise<void> {
await this.checkoutPage.fillFirstName(info.firstName);
await this.checkoutPage.fillLastName(info.lastName);
await this.checkoutPage.fillAddress(info.address);
await this.checkoutPage.fillCity(info.city);
await this.checkoutPage.fillZipCode(info.zipCode);
await this.checkoutPage.clickContinue();
}
private async fillPaymentDetails(info: PaymentInfo): Promise<void> {
await this.checkoutPage.fillCardNumber(info.cardNumber);
await this.checkoutPage.fillExpiry(info.expiry);
await this.checkoutPage.fillCVV(info.cvv);
}
}
interface ShippingInfo {
firstName: string;
lastName: string;
address: string;
city: string;
zipCode: string;
}
interface PaymentInfo {
cardNumber: string;
expiry: string;
cvv: string;
}
```
## Layer 4: Utilities
### Logger
```typescript
// src/utils/Logger.ts
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
export class Logger {
private context: string;
constructor(context: string) {
this.context = context;
}
private log(level: LogLevel, message: string, data?: unknown): void {
const timestamp = new Date().toISOString();
const entry = `[${timestamp}] [${level}] [${this.context}] ${message}`;
if (data) {
console.log(entry, JSON.stringify(data, null, 2));
} else {
console.log(entry);
}
}
debug(message: string, data?: unknown): void { this.log('DEBUG', message, data); }
info(message: string, data?: unknown): void { this.log('INFO', message, data); }
warn(message: string, data?: unknown): void { this.log('WARN', message, data); }
error(message: string, data?: unknown): void { this.log('ERROR', message, data); }
}
```
### WaitHelper
```typescript
// src/utils/WaitHelper.ts
import { Page } from '@playwright/test';
export class WaitHelper {
constructor(private page: Page) {}
async waitForCondition(
condition: () => Promise<boolean>,
timeout: number = 30_000,
interval: number = 500,
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
if (await condition()) return;
await this.page.waitForTimeout(interval);
}
throw new Error(`Condition not met within ${timeout}ms`);
}
async retry<T>(fn: () => Promise<T>, retries: number = 3, delay: number = 1000): Promise<T> {
let lastError: Error;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (i < retries - 1) await this.page.waitForTimeout(delay);
}
}
throw lastError!;
}
async waitForNetworkIdle(timeout: number = 10_000): Promise<void> {
await this.page.waitForLoadState('networkidle', { timeout });
}
}
```
### DataGenerator
```typescript
// src/utils/DataGenerator.ts
export class DataGenerator {
static randomEmail(): string {
const id = Math.random().toString(36).substring(2, 10);
return `user_${id}@test.com`;
}
static randomPhoneNumber(): string {
const num = Math.floor(1000000 + Math.random() * 9000000);
return `(555) ${String(num).slice(0, 3)}-${String(num).slice(3, 7)}`;
}
static randomString(length: number = 10): string {
return Math.random().toString(36).substring(2, 2 + length);
}
static randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
static uuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
static futureDate(daysAhead: number = 30): string {
const date = new Date();
date.setDate(date.getDate() + daysAhead);
return date.toISOString().split('T')[0];
}
}
```
### ApiHelper
```typescript
// src/utils/ApiHelper.ts
import { APIRequestContext } from '@playwright/test';
import { Logger } from './Logger';
export class ApiHelper {
private logger: Logger;
constructor(
private request: APIRequestContext,
private baseURL: string,
) {
this.logger = new Logger('ApiHelper');
}
async get<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
this.logger.info(`GET ${endpoint}`);
const response = await this.request.get(`${this.baseURL}${endpoint}`, { headers });
const body = await response.json();
this.logger.debug(`Response ${response.status()}`, body);
return body as T;
}
async post<T>(endpoint: string, data: unknown, headers?: Record<string, string>): Promise<T> {
this.logger.info(`POST ${endpoint}`);
const response = await this.request.post(`${this.baseURL}${endpoint}`, {
data,
headers,
});
const body = await response.json();
this.logger.debug(`Response ${response.status()}`, body);
return body as T;
}
async put<T>(endpoint: string, data: unknown, headers?: Record<string, string>): Promise<T> {
this.logger.info(`PUT ${endpoint}`);
const response = await this.request.put(`${this.baseURL}${endpoint}`, {
data,
headers,
});
return (await response.json()) as T;
}
async delete(endpoint: string, headers?: Record<string, string>): Promise<number> {
this.logger.info(`DELETE ${endpoint}`);
const response = await this.request.delete(`${this.baseURL}${endpoint}`, { headers });
return response.status();
}
}
```
## Layer 5: API Testing
```typescript
// src/api/AuthApi.ts
import { APIRequestContext } from '@playwright/test';
import { ApiHelper } from '@utils/ApiHelper';
import { config } from '@config/index';
export class AuthApi {
private api: ApiHelper;
constructor(request: APIRequestContext) {
this.api = new ApiHelper(request, config.baseUrl);
}
async login(username: string, password: string): Promise<{ token: string }> {
return this.api.post('/api/auth/login', { username, password });
}
async register(email: string, password: string): Promise<{ userId: string }> {
return this.api.post('/api/auth/register', { email, password });
}
async refreshToken(token: string): Promise<{ token: string }> {
return this.api.post('/api/auth/refresh', {}, { Authorization: `Bearer ${token}` });
}
}
```
```typescript
// src/api/ProductApi.ts
import { APIRequestContext } from '@playwright/test';
import { ApiHelper } from '@utils/ApiHelper';
import { config } from '@config/index';
export class ProductApi {
private api: ApiHelper;
constructor(request: APIRequestContext) {
this.api = new ApiHelper(request, config.baseUrl);
}
async getProducts(token: string): Promise<Product[]> {
return this.api.get('/api/products', { Authorization: `Bearer ${token}` });
}
async getProductById(id: string, token: string): Promise<Product> {
return this.api.get(`/api/products/${id}`, { Authorization: `Bearer ${token}` });
}
async searchProducts(query: string, token: string): Promise<Product[]> {
return this.api.get(`/api/products?q=${encodeURIComponent(query)}`, {
Authorization: `Bearer ${token}`,
});
}
}
interface Product {
id: string;
name: string;
price: number;
description: string;
}
```
## Layer 6: Custom Fixtures
Fixtures provide dependency injection. Tests never create Page or Module instances manually.
```typescript
// src/fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage, HomePage, ProductPage, CheckoutPage } from '@pages/index';
import { LoginModule, ProductModule, CheckoutModule } from '@modules/index';
type TestFixtures = {
loginPage: LoginPage;
homePage: HomePage;
productPage: ProductPage;
checkoutPage: CheckoutPage;
loginModule: LoginModule;
productModule: ProductModule;
checkoutModule: CheckoutModule;
};
export const test = base.extend<TestFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
homePage: async ({ page }, use) => {
await use(new HomePage(page));
},
productPage: async ({ page }, use) => {
await use(new ProductPage(page));
},
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
loginModule: async ({ page, loginPage }, use) => {
await use(new LoginModule(page, loginPage));
},
productModule: async ({ page, productPage }, use) => {
await use(new ProductModule(page, productPage));
},
checkoutModule: async ({ page, checkoutPage }, use) => {
await use(new CheckoutModule(page, checkoutPage));
},
});
export { expect } from '@playwright/test';
```
### Pre-Authenticated Fixture
```typescript
// src/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
import { config } from '@config/index';
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.locator('#username').fill(config.credentials.username);
await page.locator('#password').fill(config.credentials.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForLoadState('networkidle');
await use(page);
},
storageState: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/login');
await page.locator('#username').fill(config.credentials.username);
await page.locator('#password').fill(config.credentials.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForLoadState('networkidle');
const storage = await context.storageState();
await context.close();
await use(storage);
},
});
```
## Layer 7: Test Specifications
Tests use fixtures, `test.step()`, and tags. They are concise because all logic lives in Modules.
### Login Tests
```typescript
// src/tests/login.spec.ts
import { test, expect } from '@fixtures';
test.describe('@P0 @Login Login Tests', () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.navigate('/login');
});
test('should login with valid credentials', async ({ loginModule, homePage }) => {
await test.step('Login with default credentials', async () => {
await loginModule.doLogin();
});
await test.step('Verify redirect to home page', async () => {
const title = await homePage.getTitle();
console.log(`Page title: ${title}`);
expect(title).toContain('Dashboard');
});
});
test('should show error for invalid credentials', async ({ loginModule }) => {
await test.step('Attempt login with wrong password', async () => {
await loginModule.doLogin('validuser', 'wrongpassword');
});
await test.step('Verify error message displayed', async () => {
await loginModule.verifyLoginFailed('Invalid credentials');
});
});
test('should login with remember me', async ({ loginModule }) => {
await test.step('Login with Remember Me checked', async () => {
await loginModule.doLoginWithRememberMe('user@test.com', 'pass123');
});
});
});
```
### Product Tests
```typescript
// src/tests/product.spec.ts
import { test, expect } from '@fixtures';
test.describe('@P1 @Product Product Tests', () => {
test.beforeEach(async ({ loginModule }) => {
await loginModule.doLogin();
});
test('should add product to cart', async ({ productModule }) => {
await test.step('Search and add product', async () => {
await productModule.addProductToCart('Wireless Headphones', 2);
});
await test.step('Verify cart updated', async () => {
await productModule.verifyCartCount(2);
});
});
test('should search and find products', async ({ productModule }) => {
await test.step('Search for laptops', async () => {
await productModule.searchAndVerifyResults('laptop', 3);
});
});
});
```
### Checkout Tests
```typescript
// src/tests/checkout.spec.ts
import { test, expect } from '@fixtures';
import { DataGenerator } from '@utils/DataGenerator';
test.describe('@P0 @Checkout Checkout Tests', () => {
test.beforeEach(async ({ loginModule, productModule }) => {
await loginModule.doLogin();
await productModule.addProductToCart('Test Product');
});
test('should complete full checkout', async ({ checkoutModule }) => {
const orderId = await test.step('Complete checkout flow', async () => {
return await checkoutModule.completeCheckout(
{
firstName: 'John',
lastName: 'Doe',
address: '123 Test St',
city: 'Testville',
zipCode: '12345',
},
{
cardNumber: '4111111111111111',
expiry: '12/28',
cvv: '123',
},
);
});
await test.step('Verify order confirmation', async () => {
console.log(`Order placed: ${orderId}`);
expect(orderId).toBeTruthy();
});
});
});
```
## Layer 8: CI/CD Integration
### GitHub Actions
```yaml
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}
env:
BASE_URL: ${{ secrets.BASE_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.shard }}
path: |
playwright-report/
test-results/
tta-report/
```
### Docker
```dockerfile
FROM mcr.microsoft.com/playwright:v1.48.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
```
```yaml
# docker-compose.yml
services:
shard-1:
build: .
command: npx playwright test --shard=1/4
volumes:
- ./results/shard-1:/app/test-results
shard-2:
build: .
command: npx playwright test --shard=2/4
volumes:
- ./results/shard-2:/app/test-results
shard-3:
build: .
command: npx playwright test --shard=3/4
volumes:
- ./results/shard-3:/app/test-results
shard-4:
build: .
command: npx playwright test --shard=4/4
volumes:
- ./results/shard-4:/app/test-results
```
## Test Tags
Use tags for selective test execution:
| Tag | Purpose | Command |
|-----|---------|---------|
| `@P0` | Critical priority | `npx playwright test --grep @P0` |
| `@P1` | High priority | `npx playwright test --grep @P1` |
| `@P2` | Medium priority | `npx playwright test --grep @P2` |
| `@Smoke` | Smoke suite | `npx playwright test --grep @Smoke` |
| `@Regression` | Full regression | `npx playwright test --grep @Regression` |
| `@Login` | Feature-specific | `npx playwright test --grep @Login` |
## Locator Priority
Always choose locators in this order:
1. `getByRole()` -- Accessible roles (button, link, heading)
2. `getByLabel()` -- Form input labels
3. `getByPlaceholder()` -- Input placeholders
4. `getByText()` -- Visible text content
5. `getByTestId()` -- `data-testid` attributes
6. CSS selectors -- Last resort only
## Common Commands
```bash
npm test # Run all tests
npx playwright test --headed # See the browser
npx playwright test --ui # Playwright UI mode
npx playwright test --debug # Debug with inspector
npx playwright test --grep "@P0" # Run by tag
npx playwright test --project=chromium # Single browser
npx playwright test login.spec.ts # Single file
npx playwright show-report # View HTML report
npx playwright codegen # Record tests
npx playwright show-trace trace.zip # View trace
```
## Best Practices
1. **Always use `test.step()`** for every meaningful action -- it powers HTML reports and trace viewer.
2. **Locators as arrow functions** in Pages -- ensures fresh locator evaluation every call.
3. **Never put business logic in Pages** -- if you see `if/else` in a Page class, move it to a Module.
4. **Use fixtures for all setup** -- tests should never call `new LoginPage(page)` directly.
5. **Use Logger, not console.log** -- structured logging with context, levels, and timestamps.
6. **Use DataGenerator for test data** -- never hardcode emails, phone numbers, or UUIDs.
7. **Tag every test** -- `@P0`/`@P1`/`@P2` for priority, feature tags for filtering.
8. **Keep tests independent** -- each test should set up its own state via `beforeEach` and fixtures.
9. **Use path aliases** -- `@pages/LoginPage` not `../../pages/LoginPage`.
10. **Attach screenshots in steps** -- call `takeScreenshot()` at key checkpoints for debugging.
## Anti-Patterns (Never Do This)
1. **Never use `page.locator()` in a Module** -- always go through the Page class methods.
2. **Never put conditional logic in Pages** -- Pages are dumb locator containers.
3. **Never use `page.waitForTimeout()`** -- use explicit waits via WaitHelper or Playwright's built-in waits.
4. **Never skip `test.step()`** -- naked `await` sequences produce unreadable reports.
5. **Never hardcode test data** -- use DataGenerator or JSON test data files.
6. **Never create page instances in tests** -- use fixtures for dependency injection.
7. **Never use XPath when a better locator exists** -- follow the locator priority order.
8. **Never share state between tests** -- each test is isolated, period.
9. **Never use `console.log` directly** -- use the Logger utility with proper levels.
10. **Never ignore flaky tests** -- fix them immediately with proper waits and assertions.
## Code Review Checklist
Before merging any test code, verify:
**Page Class:**
- [ ] Locators defined as arrow functions
- [ ] No business logic or conditionals
- [ ] Private `page` property via constructor
- [ ] Named exports only
- [ ] Extends `BasePage`
**Module Class:**
- [ ] Uses Page class methods exclusively
- [ ] No direct `page.locator()` calls
- [ ] Logger used for step tracking
- [ ] Async/await throughout
- [ ] Error handling where needed
**Test Spec:**
- [ ] Has priority tag (`@P0`, `@P1`, `@P2`)
- [ ] Has feature tag (`@Login`, `@Checkout`)
- [ ] All actions wrapped in `test.step()`
- [ ] Uses fixtures, not manual instantiation
- [ ] Has `beforeEach` / `afterEach` if needed
- [ ] Independent from other tests
**General:**
- [ ] Files in correct layer directory
- [ ] Uses path aliases (`@pages/`, `@modules/`)
- [ ] No hardcoded values
- [ ] No `console.log` (use Logger)
- [ ] TypeScript strict mode passesRelated Skills
Advanced WebSocket Testing
WebSocket testing including connection lifecycle, reconnection logic, message ordering, backpressure handling, and binary frame testing.
Test Migration Framework
Test framework migration skill covering strategies for migrating between testing frameworks including Selenium to Playwright, Jest to Vitest, Enzyme to React Testing Library, and Protractor to Cypress with automated codemods and incremental migration patterns.
Selenium Advanced POM Framework
Advanced Selenium WebDriver framework with three Page Object Model patterns (Basic POM, Improved POM, Page Factory), retry mechanisms, Allure reporting, Excel data-driven testing, and Selenoid grid support.
Robot Framework Testing
Expert-level Robot Framework testing skill covering keyword-driven syntax, SeleniumLibrary, RequestsLibrary, custom Python keywords, data-driven testing, resource files, and parallel execution with Pabot.
REST Assured API Automation Framework
Production-grade REST API automation framework with REST Assured, POJO serialization using GSON, PayloadManager pattern, E2E integration workflows with TestNG ITestContext, and Allure reporting.
Playwright Enhanced
Advanced Playwright automation with auto-detection, custom fixtures, trace debugging, visual testing, mobile emulation, and production-grade test architecture.
Playwright Network Testing
Network-level testing with Playwright including request interception, response mocking, network throttling, and HAR recording/replay.
Playwright Mobile Web Testing
Mobile web testing skill using Playwright device emulation covering responsive testing, touch interactions, viewport management, network throttling, geolocation testing, and mobile-specific UI patterns.
Playwright E2E Testing
Comprehensive end-to-end testing skill using Playwright for web applications, covering page objects, selectors, assertions, waits, fixtures, and test organization.
Playwright CLI Browser Automation
Command-line browser automation with Playwright CLI for navigation, snapshots, uploads, downloads, tracing, and QA workflows.
Playwright CLI Automation
CLI-first browser automation using Playwright CLI for navigation, form filling, snapshots, screenshots, data extraction, and UI-flow debugging from the terminal.
Playwright API Testing
API testing skill using Playwright's built-in APIRequestContext for RESTful service validation, authentication flows, and API contract verification.