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.
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.
doLogin(), completeCheckout()). All conditional logic and multi-step flows live here.test.step() for clear HTML reporting, trace analysis, and debugging. No naked await sequences.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-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
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'] } },
],
});
{
"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/*"]
}
}
}
// 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;
Pages define locators as arrow functions and expose simple UI actions. No business logic. No conditionals. No assertions.
// 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` });
}
}
// 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() ?? '';
}
}
// 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';
}
}
// src/pages/index.ts
export { BasePage } from './BasePage';
export { LoginPage } from './LoginPage';
export { HomePage } from './HomePage';
export { ProductPage } from './ProductPage';
export { CheckoutPage } from './CheckoutPage';
Modules orchestrate Page methods into business workflows. All conditional logic, multi-step flows, and domain knowledge live here. Modules never call page.locator() directly.
// 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}`);
}
}
// 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}"`);
}
}
// 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;
}
// 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); }
}
// 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 });
}
}
// 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];
}
}
// 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();
}
}
// 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}` });
}
}
// 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;
}
Fixtures provide dependency injection. Tests never create Page or Module instances manually.
// 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';
// 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();
aw
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PramodDutta/Advanced Playwright E2E Framework下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer
Tags:playwright, e2e, advanced, page-object-model, module-pattern, fixtures, api-testing, ci-cd, docker, reporting, enterprise, framework, typescript, multi-browser, parallel-testing