Skip to main content 首页 创作者 pramoddutta qaskills advanced-playwright-e2e-framework
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.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/PramodDutta/qaskills --skill advanced-playwright-e2e-framework命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... name Advanced Playwright E2E Framework description 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. version 1.0.0 author thetestingacademy license MIT tags ["playwright","e2e","advanced","page-object-model","module-pattern","fixtures","api-testing","ci-cd","docker","reporting","enterprise","framework","typescript","multi-browser","parallel-testing"] testingTypes ["e2e","api","visual","integration","performance"] frameworks ["playwright"] languages ["typescript"] domains ["web","api"] agents ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","roo-code","augment","trae"]
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
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.
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.
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.
Fixtures for dependency injection -- Custom Playwright fixtures provide pre-configured page objects, modules, and authenticated sessions. Tests never instantiate pages or modules directly.
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 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 {
"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
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
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
import { Page } from '@playwright/test' ;
import { BasePage } from './BasePage' ;
export class LoginPage extends BasePage {
constructor (page : Page ) {
super (page);
}
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?' });
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
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
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
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
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
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
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
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
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
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
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} ` });
}
}
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.
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
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
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
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
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
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 FROM mcr.microsoft.com/playwright:v1.48.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
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 @P0Critical priority npx playwright test --grep @P0@P1High priority npx playwright test --grep @P1@P2Medium priority npx playwright test --grep @P2@SmokeSmoke suite npx playwright test --grep @Smoke@RegressionFull regression npx playwright test --grep @Regression@LoginFeature-specific npx playwright test --grep @Login
Locator Priority Always choose locators in this order:
getByRole() -- Accessible roles (button, link, heading)
getByLabel() -- Form input labels
getByPlaceholder() -- Input placeholders
getByText() -- Visible text content
getByTestId() -- data-testid attributes
CSS selectors -- Last resort only
Common Commands npm test
npx playwright test --headed
npx playwright test --ui
npx playwright test --debug
npx playwright test --grep "@P0"
npx playwright test --project=chromium
npx playwright test login.spec.ts
npx playwright show-report
npx playwright codegen
npx playwright show-trace trace.zip
Best Practices
Always use test.step() for every meaningful action -- it powers HTML reports and trace viewer.
Locators as arrow functions in Pages -- ensures fresh locator evaluation every call.
Never put business logic in Pages -- if you see if/else in a Page class, move it to a Module.
Use fixtures for all setup -- tests should never call new LoginPage(page) directly.
Use Logger, not console.log -- structured logging with context, levels, and timestamps.
Use DataGenerator for test data -- never hardcode emails, phone numbers, or UUIDs.
Tag every test -- @P0/@P1/@P2 for priority, feature tags for filtering.
Keep tests independent -- each test should set up its own state via beforeEach and fixtures.
Use path aliases -- @pages/LoginPage not ../../pages/LoginPage.
Attach screenshots in steps -- call takeScreenshot() at key checkpoints for debugging.
Anti-Patterns (Never Do This)
Never use page.locator() in a Module -- always go through the Page class methods.
Never put conditional logic in Pages -- Pages are dumb locator containers.
Never use page.waitForTimeout() -- use explicit waits via WaitHelper or Playwright's built-in waits.
Never skip test.step() -- naked await sequences produce unreadable reports.
Never hardcode test data -- use DataGenerator or JSON test data files.
Never create page instances in tests -- use fixtures for dependency injection.
Never use XPath when a better locator exists -- follow the locator priority order.
Never share state between tests -- each test is isolated, period.
Never use console.log directly -- use the Logger utility with proper levels.
Never ignore flaky tests -- fix them immediately with proper waits and assertions.
Code Review Checklist Before merging any test code, verify: