Source profileQuality 83/100Review permissions

PramodDutta/qaskills/seed-skills/playwright-advance-e2e/SKILL.md

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.

Source repository stars
195
Declared platforms
3
Static risk flags
2
Last source update
2026-08-04
Source checked
2026-08-04

Decision brief

What it does—and where it fits

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 hand…

Best for

    Not for

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

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexDeclaredSource recordInstall path and trigger
    Claude CodeDeclaredSource recordInstall path and trigger
    CursorDeclaredSource recordInstall path and trigger
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/PramodDutta/qaskills --skill "seed-skills/playwright-advance-e2e"
    Safe inspection promptEditorial

    Inspect the Agent Skill "Advanced Playwright E2E Framework" from https://github.com/PramodDutta/qaskills/blob/c924c5f7fee5fa410f267031061e492eb051757a/seed-skills/playwright-advance-e2e/SKILL.md at commit c924c5f7fee5fa410f267031061e492eb051757a. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      Code Review Checklist

      Before merging any test code, verify:

      [ ] Locators defined as arrow functions[ ] No business logic or conditionals[ ] Private page property via constructor
    2. 02

      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 exp…

      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.
    3. 03

      Test Execution Flow

      Tests import from fixtures, which provide Module and Page instances. Modules call Page methods. Pages interact with the browser. Reports capture every step.

      Tests import from fixtures, which provide Module and Page instances. Modules call Page methods. Pages interact with the browser. Reports capture every step.
    4. 04

      Project Structure

      Review the “Project Structure” section in the pinned source before continuing.

      Review and apply the “Project Structure” source section.
    5. 05

      Layer 1: Configuration

      Review the “Layer 1: Configuration” section in the pinned source before continuing.

      Review and apply the “Layer 1: Configuration” source section.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 160

    The documentation includes network, browsing, or remote request actions.

    baseUrl: process.env.BASE_URL || 'http://localhost:3000',

    Network access

    medium · line 594

    The documentation includes network, browsing, or remote request actions.

    const response = await this.request.get(`${this.baseURL}${endpoint}`, { headers });

    Runs scripts

    medium · line 987

    The documentation asks the agent to run terminal commands or scripts.

    npm test # Run all tests

    Runs scripts

    medium · line 988

    The documentation asks the agent to run terminal commands or scripts.

    npx playwright test --headed # See the browser

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score83/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars195SourceRepository attention, not individual Skill quality
    Compatibility3 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    PramodDutta/qaskills
    Skill path
    seed-skills/playwright-advance-e2e/SKILL.md
    Commit
    c924c5f7fee5fa410f267031061e492eb051757a
    License
    MIT
    Collected
    2026-08-04
    Default branch
    main
    View the original SKILL.md

    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('[email protected]', '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
    @P0Critical prioritynpx playwright test --grep @P0
    @P1High prioritynpx playwright test --grep @P1
    @P2Medium prioritynpx playwright test --grep @P2
    @SmokeSmoke suitenpx playwright test --grep @Smoke
    @RegressionFull regressionnpx playwright test --grep @Regression
    @LoginFeature-specificnpx 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

    Alternatives

    Compare before choosing