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 | 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 |
| 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 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 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 β
ββββββββββββββββββββββββββββββββββββββββ
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)
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
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 displayedStep Definition:
Then('the page title is set to {string}', async (expectedTitle: string) => {
assert.strictEqual(await page.title(), expectedTitle);
});// 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 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 1: Initialize Project
mkdir my-saas-automation
cd my-saas-automation
npm init -yStep 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.0Step 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 reportsStep 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'
});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 displayedStep 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}"`);
});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=1231. 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, '');
}- β 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.tsnotsteps1.ts
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
// 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]');
});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() || '';
}
}- 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
// 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
}
}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
slowMoin 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
- Set up project structure
- Understand hooks lifecycle
- Create base page and 1-2 page objects
- Write 5-10 basic tests
- Add more page objects
- Implement reusable step definitions
- Set up reporting
- Add test data management
- Implement test management integration (Qase/Zephyr/TestRail)
- Add CI/CD integration
- Implement parallel execution
- Performance optimization
- Code review and refactoring
- Documentation
- Team training
- Establish maintenance processes
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.