Qaskills 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.

install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/playwright-advance-e2e" ~/.claude/skills/pramoddutta-qaskills-advanced-playwright-e2e-framework && rm -rf "$T"
manifest: seed-skills/playwright-advance-e2e/SKILL.md
source content

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

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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;
}

Layer 6: Custom Fixtures

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';

Pre-Authenticated Fixture

// 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

// 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

// 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

// 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

# .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

FROM mcr.microsoft.com/playwright:v1.48.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
# 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:

TagPurposeCommand
@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

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 passes