Skip to content

Instantly share code, notes, and snippets.

@mehdi89
Created November 3, 2025 07:41
Show Gist options
  • Select an option

  • Save mehdi89/213b4abd6d82db03752778b4ac9b7ef2 to your computer and use it in GitHub Desktop.

Select an option

Save mehdi89/213b4abd6d82db03752778b4ac9b7ef2 to your computer and use it in GitHub Desktop.
qa-automation-framework-for-big-projects

Automation Framework - Complete Architecture Analysis

πŸ“‹ Executive Summary

This is a BDD (Behavior-Driven Development) E2E Testing Framework that combines:

  • Playwright for browser automation
  • Cucumber.js for BDD testing with Gherkin syntax
  • TypeScript for type-safe development
  • Qase Test Management System for test case management and reporting
  • Page Object Model (POM) design pattern for maintainability

πŸ›  Technology Stack

Core Technologies

Technology Version Purpose
@cucumber/cucumber v11.2.0 BDD test framework with Gherkin syntax
@playwright/test v1.51.1 Browser automation (Chromium)
TypeScript v5.8.2 Type-safe JavaScript development
Node.js v16+ Runtime environment
ts-node v10.9.2 TypeScript execution without compilation

Supporting Libraries

Library Purpose
qase-api-client Integration with Qase test management
cucumber-html-reporter HTML report generation
dotenv Environment variable management
axios HTTP client for API calls
cross-env Cross-platform environment variables

πŸ— System Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        QASE TEST MANAGEMENT SYSTEM                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚  Test Cases    β”‚  β”‚   Test Runs    β”‚  β”‚  Result Reporting      β”‚    β”‚
β”‚  β”‚  (@EPR-XXX)    β”‚  β”‚  (Run ID)      β”‚  β”‚  (Status, Duration)    β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    ↕ API Integration
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          CUCUMBER BDD LAYER                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  FEATURE FILES (Gherkin)                                         β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ features/                                                   β”‚   β”‚
β”‚  β”‚      β”œβ”€β”€ employee_roster/                                        β”‚   β”‚
β”‚  β”‚      β”‚   β”œβ”€β”€ employee_roster.feature        [@EPR-22]           β”‚   β”‚
β”‚  β”‚      β”‚   └── employee_detail.feature        [@EPR-45]           β”‚   β”‚
β”‚  β”‚      β”œβ”€β”€ site_administration/                                    β”‚   β”‚
β”‚  β”‚      └── payroll_reporting/                                      β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                    ↓                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  STEP DEFINITIONS (TypeScript)                                   β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ steps/                                                      β”‚   β”‚
β”‚  β”‚      β”œβ”€β”€ parameter-types.ts         (Custom param types)        β”‚   β”‚
β”‚  β”‚      β”œβ”€β”€ common_page_steps.ts       (Shared steps)              β”‚   β”‚
β”‚  β”‚      └── employee_roster/                                        β”‚   β”‚
β”‚  β”‚          └── employee_roster_steps.ts                           β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        PAGE OBJECT MODEL LAYER                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  BASE PAGE (BasePage)                                            β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ Dynamic page getter (always current)                        β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ Navigation methods                                          β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ Common elements (nav bar, account menu)                     β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ Utility methods (waitForElement, scrollDropdown)           β”‚   β”‚
β”‚  β”‚  └── Tab/Dialog management                                       β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                    ↓ extends                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  PAGE OBJECTS (Specific Pages)                                   β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ employee_roster_page.ts                                     β”‚   β”‚
β”‚  β”‚  β”‚   β”œβ”€β”€ Elements { private locators }                           β”‚   β”‚
β”‚  β”‚  β”‚   β”œβ”€β”€ Display methods (xxxDisplayed(): boolean)              β”‚   β”‚
β”‚  β”‚  β”‚   β”œβ”€β”€ Action methods (clickXxx(), fillXxx())                 β”‚   β”‚
β”‚  β”‚  β”‚   └── Helper methods (custom logic)                           β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ login_page.ts                                               β”‚   β”‚
β”‚  β”‚  └── ... (other page objects)                                    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       PLAYWRIGHT BROWSER LAYER                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  HOOKS (Browser Lifecycle)                                       β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ BeforeAll:  Launch browser (single instance)               β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ Before:     Create fresh context + page per scenario       β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ After:      Save trace, cleanup context                    β”‚   β”‚
β”‚  β”‚  └── AfterAll:   Close browser                                   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  QASE HOOKS (Test Management)                                    β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ BeforeAll:  Validate EPR tags, filter tests                β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ Before:     Check if test should execute                   β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ AfterStep:  Track step execution                           β”‚   β”‚
β”‚  β”‚  β”œβ”€β”€ After:      Report results to Qase                         β”‚   β”‚
β”‚  β”‚  └── AfterAll:   Display summary                                 β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         SUPPORT/HELPER LAYER                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Context    β”‚  β”‚ Qase Client  β”‚  β”‚ Test Filter  β”‚  β”‚ Functions β”‚   β”‚
β”‚  β”‚  (Shared     β”‚  β”‚ (API calls)  β”‚  β”‚ (Filtering)  β”‚  β”‚ (Utility) β”‚   β”‚
β”‚  β”‚   State)     β”‚  β”‚              β”‚  β”‚              β”‚  β”‚           β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           REPORTING LAYER                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚ Cucumber     β”‚  β”‚  Playwright  β”‚  β”‚    Qase      β”‚                  β”‚
β”‚  β”‚ HTML Report  β”‚  β”‚  Trace View  β”‚  β”‚   Dashboard  β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“ Project Structure Deep Dive

eprlive-automation/
β”‚
β”œβ”€β”€ src/test/
β”‚   β”‚
β”‚   β”œβ”€β”€ features/                          # BDD Feature Files (Gherkin)
β”‚   β”‚   β”œβ”€β”€ employee_roster/
β”‚   β”‚   β”‚   β”œβ”€β”€ employee_roster.feature         # Feature: functional area
β”‚   β”‚   β”‚   └── employee_detail.feature         # Split by workflow
β”‚   β”‚   β”œβ”€β”€ site_administration/
β”‚   β”‚   β”œβ”€β”€ payroll_reporting/
β”‚   β”‚   └── ...
β”‚   β”‚
β”‚   β”œβ”€β”€ steps/                             # Step Definitions (Glue Code)
β”‚   β”‚   β”œβ”€β”€ parameter-types.ts                  # Custom Cucumber params
β”‚   β”‚   β”œβ”€β”€ common_page_steps.ts                # Shared/generic steps
β”‚   β”‚   └── employee_roster/
β”‚   β”‚       └── employee_roster_steps.ts        # Page-specific steps
β”‚   β”‚
β”‚   β”œβ”€β”€ pages/                             # Page Object Model
β”‚   β”‚   β”œβ”€β”€ base_page.ts                        # Base class with common logic
β”‚   β”‚   β”œβ”€β”€ employee_roster_page.ts             # Specific page objects
β”‚   β”‚   β”œβ”€β”€ login_page.ts
β”‚   β”‚   └── ...
β”‚   β”‚
β”‚   β”œβ”€β”€ hooks/                             # Lifecycle Management
β”‚   β”‚   β”œβ”€β”€ hooks.ts                            # Browser lifecycle
β”‚   β”‚   └── qase-hooks.ts                       # Qase integration
β”‚   β”‚
β”‚   β”œβ”€β”€ helpers/                           # Utilities & Support
β”‚   β”‚   β”œβ”€β”€ context.ts                          # Shared state between steps
β”‚   β”‚   β”œβ”€β”€ qase-client.ts                      # Qase API client
β”‚   β”‚   β”œβ”€β”€ test-filter.ts                      # Test filtering logic
β”‚   β”‚   β”œβ”€β”€ functions.ts                        # Utility functions
β”‚   β”‚   └── extensions/
β”‚   β”‚       β”œβ”€β”€ locator-extensions.ts           # Custom Playwright extensions
β”‚   β”‚       └── locator-extensions.d.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ config/                            # Configuration
β”‚   β”‚   └── qase-config.ts                      # Qase settings
β”‚   β”‚
β”‚   β”œβ”€β”€ data/                              # Test Data
β”‚   β”‚   └── general-data.ts                     # Static test data
β”‚   β”‚
β”‚   └── models/                            # Data Models
β”‚       β”œβ”€β”€ employee_roster_row.ts              # Table row models
β”‚       └── ...
β”‚
β”œβ”€β”€ reports/                               # Generated Reports
β”‚   β”œβ”€β”€ cucumber-report.json                    # Cucumber JSON output
β”‚   β”œβ”€β”€ cucumber-report.html                    # HTML report
β”‚   └── trace-latest.zip                        # Playwright trace
β”‚
β”œβ”€β”€ cucumber.json                          # Cucumber configuration
β”œβ”€β”€ tsconfig.json                          # TypeScript configuration
β”œβ”€β”€ package.json                           # Dependencies & scripts
β”œβ”€β”€ .env                                   # Environment variables
β”œβ”€β”€ generate-report.js                     # Report generator script
└── open-report.js                         # Report opener script

πŸ”„ Test Execution Workflow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          TEST EXECUTION FLOW                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1. INITIALIZATION (BeforeAll Hooks)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Qase Hooks: BeforeAll                β”‚
   β”‚ β”œβ”€β”€ Validate EPR tag uniqueness      β”‚ ← Prevents duplicate test IDs
   β”‚ β”œβ”€β”€ Initialize Qase client           β”‚
   β”‚ β”œβ”€β”€ Validate test run exists         β”‚
   β”‚ β”œβ”€β”€ Fetch test cases from run        β”‚
   β”‚ β”œβ”€β”€ Apply skip-passed filtering      β”‚
   β”‚ └── Categorize automated/manual      β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Browser Hooks: BeforeAll             β”‚
   β”‚ └── Launch Chromium browser          β”‚ ← Single instance for all tests
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. SCENARIO SETUP (Before Hooks)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Qase Hooks: Before                   β”‚
   β”‚ β”œβ”€β”€ Check if should execute          β”‚ ← Filter by Qase run
   β”‚ └── Initialize step tracking         β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Browser Hooks: Before                β”‚
   β”‚ β”œβ”€β”€ Create fresh context             β”‚ ← Isolated environment
   β”‚ β”œβ”€β”€ Start tracing                    β”‚
   β”‚ β”œβ”€β”€ Create new page                  β”‚
   β”‚ └── Apply extensions                 β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. SCENARIO EXECUTION
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Feature File (Gherkin)               β”‚
   β”‚   Given user logs in as "admin"      β”‚
   β”‚   When user clicks "Add Employee"    β”‚
   β”‚   Then modal is displayed            β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Step Definitions                     β”‚
   β”‚ β”œβ”€β”€ Parse step parameters            β”‚
   β”‚ β”œβ”€β”€ Call page object methods         β”‚
   β”‚ └── Assert expected results          β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Page Objects                         β”‚
   β”‚ β”œβ”€β”€ Locate elements                  β”‚
   β”‚ β”œβ”€β”€ Perform actions                  β”‚
   β”‚ └── Return results                   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Playwright (Browser Automation)      β”‚
   β”‚ β”œβ”€β”€ Execute browser actions          β”‚
   β”‚ β”œβ”€β”€ Wait for elements                β”‚
   β”‚ └── Capture screenshots              β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. STEP TRACKING (Step Hooks)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ BeforeStep: Record step text         β”‚
   β”‚ AfterStep:  Record status & errors   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

5. SCENARIO CLEANUP (After Hooks)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Qase Hooks: After                    β”‚
   β”‚ β”œβ”€β”€ Calculate duration               β”‚
   β”‚ β”œβ”€β”€ Generate execution report        β”‚
   β”‚ β”œβ”€β”€ Map status (pass/fail)           β”‚
   β”‚ └── Report result to Qase            β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Browser Hooks: After                 β”‚
   β”‚ β”œβ”€β”€ Stop tracing & save              β”‚
   β”‚ β”œβ”€β”€ Clear cookies/permissions        β”‚
   β”‚ β”œβ”€β”€ Close context & page             β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6. FINALIZATION (AfterAll Hooks)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Qase Hooks: AfterAll                 β”‚
   β”‚ └── Display reporting summary        β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Browser Hooks: AfterAll              β”‚
   β”‚ β”œβ”€β”€ Clean downloads folder           β”‚
   β”‚ └── Close browser                    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

7. REPORTING
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Generate HTML Report                 β”‚
   β”‚ β”œβ”€β”€ cucumber-report.json             β”‚
   β”‚ └── cucumber-report.html             β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🎯 Key Design Patterns & Principles

1. Page Object Model (POM)

Structure:

class EmployeeRosterPage extends BasePage {
  // 1. Private Elements (Locators)
  private Elements = {
    addButton: () => this.page.locator("#btnAdd"),
    employeeTable: () => this.page.locator("#gvwEmployees"),
  }

  // 2. Display Methods (boolean returns)
  async addButtonDisplayed(): Promise<boolean> {
    return await this.Elements.addButton().isVisible();
  }

  // 3. Action Methods (user interactions)
  async clickAddButton(): Promise<void> {
    await this.Elements.addButton().click();
  }

  // 4. Helper Methods (complex logic)
  async getAllEmployees(): Promise<EmployeeRow[]> {
    // Custom logic
  }
}

Benefits:

  • βœ… Single source of truth for element locators
  • βœ… Reusable methods across multiple tests
  • βœ… Easy maintenance when UI changes
  • βœ… Separation of concerns (UI logic vs test logic)

2. Dynamic Page Getter Pattern

export class BasePage {
  // Dynamic getter that always returns current page from hooks
  protected get page(): Page {
    const hooks = require('../hooks/hooks');
    return hooks.page;
  }
}

Why this matters:

  • No constructor needed in page objects
  • Automatically works when browser context changes
  • Supports fresh context creation mid-test
  • Perfect for cookie/session validation tests

3. BDD with Cucumber & Gherkin

Feature File:

Feature: Employee Roster

  Background:
    Given user logs in as "admin"
    And user navigates to "Employee Roster"

  @EPR-22
  Scenario: Employee Roster - Smoke Test
    #Step 1: Validate page structure
    Then the page title is set to "Employee Roster"
    And navigation bar is displayed

Step Definition:

Then('the page title is set to {string}', async (expectedTitle: string) => {
  assert.strictEqual(await page.title(), expectedTitle);
});

4. Test Context Pattern

// Store data in one step
Context.set('employerId', 12345);

// Retrieve in another step
const id = Context.get<number>('employerId');

Use cases:

  • Share data between steps in same scenario
  • Store dynamically generated IDs
  • Pass data from setup to verification steps

πŸ§ͺ Qase Integration - How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        QASE INTEGRATION FLOW                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1. TEST CASE MAPPING (1:1 Relationship)
   
   Qase Test Case:                    Feature File:
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ EPR-22            β”‚    ←────→   β”‚ @EPR-22              β”‚
   β”‚ Employee Roster   β”‚             β”‚ Scenario: ...        β”‚
   β”‚ Smoke Test        β”‚             β”‚                      β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. EPR TAG VALIDATION (BeforeAll)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Scan all .feature files              β”‚
   β”‚ Extract @EPR-XXX tags                β”‚
   β”‚ Check for duplicates                 β”‚
   β”‚ β”œβ”€β”€ If duplicates found: ABORT       β”‚ ← Ensures 1:1 mapping
   β”‚ └── If unique: CONTINUE              β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. TEST RUN FILTERING (BeforeAll)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ QASE_RUN_ID provided?                β”‚
   β”‚ β”œβ”€β”€ Yes: Fetch test cases from run  β”‚
   β”‚ β”‚   β”œβ”€β”€ Apply skip-passed filter    β”‚
   β”‚ β”‚   β”œβ”€β”€ Categorize automated/manual β”‚
   β”‚ β”‚   └── Only execute automated      β”‚
   β”‚ └── No: Execute all tests            β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. SKIP-PASSED OPTIMIZATION
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ QASE_SKIP_PASSED=true?               β”‚
   β”‚ β”œβ”€β”€ Yes:                             β”‚
   β”‚ β”‚   β”œβ”€β”€ Fetch existing results      β”‚
   β”‚ β”‚   β”œβ”€β”€ Filter out "passed" tests   β”‚
   β”‚ β”‚   └── Execute remaining only      β”‚
   β”‚ └── No: Execute all tests in run    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   
   Example Analysis:
   ═══════════════════════════
   Total tests in run: 45
   Tests to SKIP: 28 (passed)
   Tests to RE-EXECUTE: 12
   Tests NOT AUTOMATED: 5
   ═══════════════════════════

5. STEP-LEVEL TRACKING (During Execution)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ BeforeStep: Capture step text        β”‚
   β”‚ AfterStep:  Capture status & error   β”‚
   β”‚ Build execution log:                 β”‚
   β”‚   βœ… Given user logs in              β”‚
   β”‚   βœ… When user clicks button         β”‚
   β”‚   ❌ Then modal is displayed         β”‚
   β”‚   Error: Timeout 10000ms...          β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6. RESULT REPORTING (After Scenario)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Calculate duration                   β”‚
   β”‚ Map status (passed/failed/skipped)   β”‚
   β”‚ Generate detailed comment:           β”‚
   β”‚   - Step-by-step execution log       β”‚
   β”‚   - Error messages & stack traces    β”‚
   β”‚ Report to Qase API                   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸš€ Step-by-Step Implementation Guide for Another SaaS App

Phase 1: Project Setup (Day 1)

Step 1: Initialize Project

mkdir my-saas-automation
cd my-saas-automation
npm init -y

Step 2: Install Dependencies

# Core dependencies
npm install --save-dev @cucumber/cucumber@^11.2.0
npm install --save-dev @playwright/test@^1.51.1
npm install --save-dev typescript@^5.8.2
npm install --save-dev ts-node@^10.9.2
npm install --save-dev @types/node@^22.13.10

# Reporting & utilities
npm install --save-dev cucumber-html-reporter@^6.0.0
npm install --save-dev cross-env@^7.0.3

# Environment management
npm install dotenv@^16.4.7

# Optional: Test management integration
npm install --save-dev qase-api-client@^1.0.2
npm install --save-dev axios@^1.6.0

Step 3: Create TypeScript Configuration

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "types": ["node", "@playwright/test"]
  },
  "exclude": ["node_modules"]
}

Step 4: Create Cucumber Configuration

Create cucumber.json:

{
  "default": {
    "paths": [
      "src/test/features/**/*.feature"
    ],
    "require": [
      "src/test/steps/parameter-types.ts",
      "src/test/steps/**/*.ts",
      "src/test/hooks/**/*.ts"
    ],
    "requireModule": [
      "ts-node/register"
    ],
    "format": [
      "summary",
      "json:reports/cucumber-report.json"
    ],
    "formatOptions": {
      "snippetInterface": "async-await"
    },
    "dryRun": false
  }
}

Step 5: Update package.json Scripts

{
  "scripts": {
    "test": "cucumber-js --config cucumber.json --tags @focus",
    "test:debug": "cross-env HEADED=true cucumber-js --config cucumber.json --tags @focus",
    "tests": "cucumber-js --config cucumber.json",
    "tests:tag": "npm run tests -- --tags",
    "tests:headed": "cross-env HEADED=true cucumber-js --config cucumber.json",
    "report": "node generate-report.js && node open-report.js",
    "trace": "npx playwright show-trace reports/trace-latest.zip"
  }
}

Step 6: Create Directory Structure

mkdir -p src/test/{features,steps,pages,hooks,helpers,config,data,models}
mkdir -p reports

Phase 2: Core Framework Setup (Day 2-3)

Step 1: Create Base Page

Create src/test/pages/base_page.ts:

import { Page, Locator } from "@playwright/test";

export class BasePage {
  // Dynamic getter for current page
  protected get page(): Page {
    const hooks = require('../hooks/hooks');
    return hooks.page;
  }

  // Common navigation methods
  async navigateTo(url: string): Promise<void> {
    await this.page.goto(url);
    await this.page.waitForLoadState("networkidle");
  }

  async refreshPage(): Promise<void> {
    await this.page.reload({ waitUntil: "domcontentloaded" });
  }

  // Common wait utilities
  async waitForElement(
    element: Locator, 
    displayed: boolean = true, 
    timeout: number = 5000
  ): Promise<void> {
    try {
      await element.first().waitFor({ 
        state: displayed ? 'visible' : 'hidden',  
        timeout: timeout 
      });
    } catch (error) {
      // Handle timeout gracefully
    }
  }

  async isElementVisible(locator: Locator): Promise<boolean> {
    try {
      return await locator.isVisible({ timeout: 300 });
    } catch {
      return false;
    }
  }

  // Navigation
  async getPageTitle(): Promise<string> {
    return await this.page.title();
  }

  async getCurrentUrl(): Promise<string> {
    return this.page.url();
  }

  async urlContains(urlFragment: string): Promise<boolean> {
    return (await this.getCurrentUrl()).includes(urlFragment);
  }
}

Step 2: Create Browser Hooks

Create src/test/hooks/hooks.ts:

import {
  Before,
  After,
  BeforeAll,
  AfterAll,
  setDefaultTimeout,
} from "@cucumber/cucumber";
import { Browser, BrowserContext, Page, chromium } from "@playwright/test";
import dotenv from "dotenv";
import * as fs from 'fs';

dotenv.config();
setDefaultTimeout(10000);

export let browser: Browser;
export let context: BrowserContext;
export let page: Page;

BeforeAll(async () => {
  const isHeaded = process.env.HEADED === 'true';
  browser = await chromium.launch({ 
    headless: !isHeaded,
    slowMo: 35
  });
});

Before(async function() {
  context = await browser.newContext({
    viewport: { width: 1920, height: 1080 }
  });
  
  await context.tracing.start({ 
    screenshots: true, 
    snapshots: true,
    sources: true,
    title: this.pickle?.name || 'Unknown scenario'
  });
  
  page = await context.newPage();
});

After(async function(scenario) {
  if (!fs.existsSync('reports')) { 
    fs.mkdirSync('reports', { recursive: true });
  }
  
  const tracePath = `reports/trace-latest.zip`;
  await context.tracing.stop({ path: tracePath });
  
  await context.clearCookies();
  await context.close();
  await page.close();
});

AfterAll(async () => {
  await browser.close();
});

Step 3: Create Context Helper

Create src/test/helpers/context.ts:

export class Context {
  private static store: Map<string, any> = new Map();

  static set(key: string, value: any): void {
    this.store.set(key, value);
  }

  static get<T>(key: string): T {
    if (!this.store.has(key)) {
      throw new Error(`Key "${key}" not found in context.`);
    }
    return this.store.get(key) as T;
  }

  static has(key: string): boolean {
    return this.store.has(key);
  }

  static clear(key: string): void {
    this.store.delete(key);
  }
}

Step 4: Create Parameter Types

Create src/test/steps/parameter-types.ts:

import { defineParameterType } from '@cucumber/cucumber';

// Boolean parameter
defineParameterType({
  name: 'boolean',
  regexp: /true|false/,
  transformer: (s: string) => s === 'true'
});

Phase 3: Implement Your First Test (Day 4-5)

Step 1: Create Login Page Object

Create src/test/pages/login_page.ts:

import { BasePage } from "./base_page";

export class LoginPage extends BasePage {
  private Elements = {
    emailInput: () => this.page.locator('#email'),
    passwordInput: () => this.page.locator('#password'),
    loginButton: () => this.page.getByRole('button', { name: 'Login' }),
    errorMessage: () => this.page.locator('.error-message'),
  }

  async navigateToLogin(): Promise<void> {
    await this.navigateTo(process.env.BASE_URL + '/login');
  }

  async fillEmail(email: string): Promise<void> {
    await this.Elements.emailInput().fill(email);
  }

  async fillPassword(password: string): Promise<void> {
    await this.Elements.passwordInput().fill(password);
  }

  async clickLoginButton(): Promise<void> {
    await this.Elements.loginButton().click();
  }

  async login(email: string, password: string): Promise<void> {
    await this.fillEmail(email);
    await this.fillPassword(password);
    await this.clickLoginButton();
  }

  async errorMessageDisplayed(): Promise<boolean> {
    return await this.Elements.errorMessage().isVisible();
  }
}

Step 2: Create Login Step Definitions

Create src/test/steps/login_steps.ts:

import { Given, When, Then, Before } from '@cucumber/cucumber';
import assert from 'assert';
import { LoginPage } from '../pages/login_page';

let loginPage: LoginPage;

Before(async () => {
  loginPage = new LoginPage();
});

Given('user is on the login page', async () => {
  await loginPage.navigateToLogin();
});

When('user logs in with email {string} and password {string}', 
  async (email: string, password: string) => {
    await loginPage.login(email, password);
});

Then('user is redirected to dashboard', async () => {
  assert.ok(await loginPage.urlContains('/dashboard'), 
    'User was not redirected to dashboard');
});

Then('login error message is displayed', async () => {
  assert.ok(await loginPage.errorMessageDisplayed(), 
    'Error message was not displayed');
});

Step 3: Create Login Feature

Create src/test/features/authentication/login.feature:

@authentication
Feature: User Login

  Background:
    Given user is on the login page

  @TEST-001 @smoke
  Scenario: Successful login with valid credentials
    When user logs in with email "[email protected]" and password "Password123!"
    Then user is redirected to dashboard

  @TEST-002
  Scenario: Failed login with invalid credentials
    When user logs in with email "[email protected]" and password "wrong"
    Then login error message is displayed

Step 4: Create Common Steps

Create src/test/steps/common_steps.ts:

import { Then } from '@cucumber/cucumber';
import { page } from '../hooks/hooks';
import assert from 'assert';

Then('the page title is set to {string}', async (expectedTitle: string) => {
  const actualTitle = await page.title();
  assert.strictEqual(actualTitle, expectedTitle, 
    `Page title "${actualTitle}" does not match expected "${expectedTitle}"`);
});

Then('the url contains {string}', async (urlFragment: string) => {
  const currentUrl = page.url();
  assert.ok(currentUrl.includes(urlFragment), 
    `URL "${currentUrl}" does not contain "${urlFragment}"`);
});

Phase 4: Reporting Setup (Day 6)

Step 1: Create Report Generator

Create generate-report.js:

const reporter = require('cucumber-html-reporter');
const fs = require('fs');
const path = require('path');

const reportsDir = path.join(__dirname, 'reports');
if (!fs.existsSync(reportsDir)) {
    fs.mkdirSync(reportsDir, { recursive: true });
}

const options = {
    theme: 'bootstrap',
    jsonFile: 'reports/cucumber-report.json',
    output: 'reports/cucumber-report.html',
    reportSuiteAsScenarios: true,
    scenarioTimestamp: true,
    launchReport: false,
    metadata: {
        "App Version": "1.0.0",
        "Test Environment": process.env.TEST_ENV || "LOCAL",
        "Browser": "Chromium",
        "Platform": process.platform
    },
    brandTitle: "My SaaS E2E Test Report",
    name: "Automation Tests"
};

if (!fs.existsSync(options.jsonFile)) {
    console.error('JSON report file not found. Run tests first.');
    process.exit(1);
}

try {
    console.log('Generating HTML report...');
    reporter.generate(options);
    console.log('Report generated:', path.resolve(options.output));
} catch (error) {
    console.error('Error generating report:', error.message);
    process.exit(1);
}

Step 2: Create Report Opener

Create open-report.js:

const open = require('open');
const path = require('path');

const reportPath = path.join(__dirname, 'reports', 'cucumber-report.html');
open(reportPath);

Step 3: Create Environment File

Create .env:

BASE_URL=https://your-saas-app.com
HEADED=false
TEST_ENV=LOCAL

# Optional: Test Management Integration
# TMS_API_TOKEN=your_token_here
# TMS_RUN_ID=123

Phase 5: Advanced Patterns (Day 7-10)

1. Data Models for Tables

Create src/test/models/user_row.ts:

import { Page, Locator } from "@playwright/test";

export class UserRow {
  private page: Page;
  private rowIndex: number;

  constructor(page: Page, rowIndex: number) {
    this.page = page;
    this.rowIndex = rowIndex;
  }

  private rowLocator(): Locator {
    return this.page.locator(`table tbody tr`).nth(this.rowIndex);
  }

  async getName(): Promise<string> {
    return await this.rowLocator().locator('td').nth(0).textContent() || '';
  }

  async getEmail(): Promise<string> {
    return await this.rowLocator().locator('td').nth(1).textContent() || '';
  }

  async clickEditButton(): Promise<void> {
    await this.rowLocator().getByRole('button', { name: 'Edit' }).click();
  }
}

2. Test Data Management

Create src/test/data/test-users.ts:

export const TestUsers = {
  ADMIN: {
    email: '[email protected]',
    password: 'Admin123!',
    role: 'admin'
  },
  REGULAR_USER: {
    email: '[email protected]',
    password: 'User123!',
    role: 'user'
  },
  VIEWER: {
    email: '[email protected]',
    password: 'Viewer123!',
    role: 'viewer'
  }
};

3. Reusable Helper Functions

Create src/test/helpers/functions.ts:

export function generateRandomEmail(): string {
  const timestamp = Date.now();
  return `test.user.${timestamp}@example.com`;
}

export function formatDate(date: Date, format: string = "YYYY-MM-DD"): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  
  return format
    .replace('YYYY', String(year))
    .replace('MM', month)
    .replace('DD', day);
}

export function stripAnsiCodes(text: string): string {
  return text.replace(/\x1B\[[0-9;]*m/g, '');
}

πŸ“Š Best Practices & Recommendations

1. File Organization Rules

  • βœ… Feature files: Split by functional workflow, not by page
  • βœ… Step files: Consolidate by suite/module
  • βœ… Page files: One page object per logical page
  • βœ… Use meaningful names: user_management_steps.ts not steps1.ts

2. Naming Conventions

Feature Files:     user_management_create_user.feature
Step Files:        user_management_steps.ts
Page Files:        user_management_page.ts
Test IDs:          @TEST-123 or @EPR-456

3. Step Definition Patterns

// Good: Descriptive and reusable
When('user enters {string} in the email field', async (email: string) => {
  await page.fillEmail(email);
});

// Bad: Too specific, not reusable
When('user enters [email protected] in the email field', async () => {
  await page.fillEmail('[email protected]');
});

4. Page Object Best Practices

class MyPage extends BasePage {
  // Private locators (encapsulation)
  private Elements = {
    button: () => this.page.locator('#btn')
  }

  // Public methods (business actions)
  async clickButton(): Promise<void> {
    await this.waitForElement(this.Elements.button());
    await this.Elements.button().click();
  }

  // Return values for assertions
  async getButtonText(): Promise<string> {
    return await this.Elements.button().textContent() || '';
  }
}

5. Test Data Management

  • Use environment variables for credentials
  • Create test data factories for complex objects
  • Clean up test data after tests (if applicable)
  • Use unique identifiers to avoid conflicts

6. Error Handling

// Wrap assertions with meaningful messages
assert.strictEqual(actualValue, expectedValue, 
  `Expected ${expectedValue} but got ${actualValue}`);

// Use try-catch for optional operations
async isElementVisible(locator: Locator): Promise<boolean> {
  try {
    return await locator.isVisible({ timeout: 300 });
  } catch {
    return false; // Element doesn't exist
  }
}

πŸ”§ Troubleshooting Guide

Common Issues

Issue: "Cannot find module '../hooks/hooks'"

  • Solution: Ensure TypeScript compilation works: npx tsc --noEmit

Issue: "Element not found timeout"

  • Solution: Add explicit waits before interactions:
await this.waitForElement(locator);
await locator.click();

Issue: "Tests running too fast / not reliable"

  • Solution: Increase slowMo in browser launch options:
browser = await chromium.launch({ slowMo: 100 });

Issue: "Page object methods return undefined"

  • Solution: Ensure all async functions are awaited
  • Check that page getter is returning valid page instance

πŸŽ“ Learning Path

Week 1: Foundation

  • Set up project structure
  • Understand hooks lifecycle
  • Create base page and 1-2 page objects
  • Write 5-10 basic tests

Week 2: Expansion

  • Add more page objects
  • Implement reusable step definitions
  • Set up reporting
  • Add test data management

Week 3: Advanced

  • Implement test management integration (Qase/Zephyr/TestRail)
  • Add CI/CD integration
  • Implement parallel execution
  • Performance optimization

Week 4: Refinement

  • Code review and refactoring
  • Documentation
  • Team training
  • Establish maintenance processes

🚦 Summary

This framework provides:

βœ… Clear separation of concerns (Features β†’ Steps β†’ Pages β†’ Browser)
βœ… Reusability through Page Object Model
βœ… Maintainability with TypeScript and strict patterns
βœ… Traceability with test management integration
βœ… Rich reporting via Cucumber HTML + Playwright traces
βœ… Scalability for growing test suites

Key Takeaway: This architecture is technology-agnostic. You can replicate it for ANY web application by simply changing the locators, URLs, and business logic in page objects while keeping the core framework structure intact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment