diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml
index a1e68ed7e6..01b8e4bfeb 100644
--- a/.github/workflows/e2e-playwright-test.yml
+++ b/.github/workflows/e2e-playwright-test.yml
@@ -1,4 +1,6 @@
name: Next-CRM Playwright Tests
+permissions:
+ contents: read
# Controls when the workflow will run
on:
@@ -16,11 +18,14 @@ jobs:
test:
if: |
- (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') || success()
- runs-on: self-hosted
- container:
- image: ubuntu:24.04
- environment: Stage # Retrive secrets from 'Stage' environment
+ github.event_name == 'workflow_dispatch' ||
+ github.event_name == 'schedule' ||
+ (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')
+ runs-on: ubuntu-latest
+ environment: Stage # Retrieve secrets from 'Stage' environment
+ defaults:
+ run:
+ working-directory: tests/e2e
env:
BASE_URL: ${{ secrets.BASE_URL }}
@@ -43,13 +48,21 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version-file: "frontend/.nvmrc"
+ node-version: "18"
+
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: "temurin"
+ java-version: "17"
- name: Cache npm modules
uses: actions/cache@v4
with:
- path: /github/home/.npm
- key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
+ path: |
+ ~/.npm
+ tests/e2e/node_modules
+ key: ${{ runner.os }}-node-${{ hashFiles('tests/e2e/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
@@ -60,17 +73,14 @@ jobs:
node --version
npm --version
- - name: Install prerequisites
- run: |
- apt-get update
- apt-get install -y curl sudo
-
- name: Sanitize branch name
shell: bash
+ env:
+ UNTRUSTED_PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
- if [ "${{ github.event_name }}" = "pull_request" ]; then
- # PR → use the source branch name
- RAW_BRANCH="${{ github.head_ref }}"
+ if [ "${{ github.event_name }}" = "pull_request_review" ]; then
+ # PR Review → use the source branch name
+ RAW_BRANCH="$UNTRUSTED_PR_HEAD_REF"
else
# workflow_dispatch or schedule → use whatever ref we were on
RAW_BRANCH="${GITHUB_REF#refs/heads/}"
@@ -79,17 +89,18 @@ jobs:
SAFE_BRANCH="${RAW_BRANCH//\//-}"
echo "SANITIZED_REF=${SAFE_BRANCH}" >> $GITHUB_ENV
echo "→ Restoring/uploading history under key: ${SAFE_BRANCH}"
-
- name: Debug—print refs
run: |
echo "EVENT_NAME = ${{ github.event_name }}"
echo "GITHUB_REF = $GITHUB_REF"
echo "SANITIZED_REF = $SANITIZED_REF"
+ echo "WORKING_DIRECTORY = $(pwd)"
- name: Create local allure-history/history folder
run: mkdir -p allure-history/history
- name: Install MinIO Client (mc)
+ working-directory: ${{ github.workspace }}
run: |
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc \
-o mc && chmod +x mc && sudo mv mc /usr/local/bin/
@@ -102,6 +113,7 @@ jobs:
${{ env.HETZNER_S3_SECRET_ACCESS_KEY }}
- name: Restore Allure history for this branch (mc)
+ working-directory: tests/e2e
run: |
if mc ls hetzner/${{ env.HETZNER_S3_BUCKET }}/${{ env.SANITIZED_REF }}/history/ | grep -q .; then
echo "Restoring previous Allure history..."
@@ -119,19 +131,10 @@ jobs:
- name: Install dependencies
run: |
npm ci
- cd tests/e2e/
npx playwright install --with-deps
- - name: Setup Java
- uses: actions/setup-java@v3
- with:
- distribution: "temurin"
- java-version: "17"
-
- name: Run e2e Tests.
- run: |
- cd tests/e2e/
- npm run e2e:tests --
+ run: npm run e2e:tests --
- name: Restore previous Allure history into results
if: always()
@@ -141,25 +144,19 @@ jobs:
- name: Generate Allure (for count extraction)
if: always()
- run: |
- cd tests/e2e
- # clean & generate for count-extraction
- npm run allure:report
+ run: npm run allure:report
- name: Merge history into results
if: always()
- run: |
-
- cp -R allure-report-for-count-extraction/history/* allure-results/history/
+ run: cp -R allure-report-for-count-extraction/history/* allure-results/history/
- name: Generate single-file Allure HTML
if: always()
- run: |
- cd tests/e2e
- npm run allure:htmlReport
+ run: npm run allure:htmlReport
- name: Upload updated Allure history for this branch (mc)
if: always()
+ working-directory: tests/e2e
run: |
mc cp --recursive \
allure-results/history/ \
@@ -169,7 +166,7 @@ jobs:
if: ${{ success() || failure() }}
run: |
# Install jq
- apt-get update && apt-get install -y jq
+ sudo apt-get update && sudo apt-get install -y jq
# Path to the Allure summary file
SUMMARY_FILE="allure-report-for-count-extraction/widgets/summary.json"
@@ -217,7 +214,7 @@ jobs:
if: always()
run: |
echo "PWD: $(pwd)"
- echo "Root contents:"
+ echo "Contents of current directory:"
ls -1
echo "Contents of allure-history/:"
ls -R allure-history || echo "(not found or empty)"
@@ -227,19 +224,18 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: Allure-report
- path: allure-report
+ path: tests/e2e/allure-report
- name: Zip Allure Report
if: ${{ success() || failure() }}
run: |
- apt-get update && apt-get install -y zip
+ sudo apt-get update && sudo apt-get install -y zip
REPORT_DIR="allure-report"
OUTPUT_ZIP="report.zip"
if [ -d "$REPORT_DIR" ]; then
echo "Zipping Allure report..."
- cd "$(dirname "$REPORT_DIR")"
- zip -r "$OUTPUT_ZIP" "$(basename "$REPORT_DIR")"
+ zip -r "$OUTPUT_ZIP" "$REPORT_DIR"
else
echo "Error - Allure Report not found in $REPORT_DIR"
exit 1
@@ -247,13 +243,17 @@ jobs:
- name: Set the email body message for pull request merge or schedule message
if: ${{ success() || failure() }}
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ PR_URL: ${{ github.event.pull_request.html_url }}
run: |
- if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
- echo "message=
For the latest pull request merge titled ${{ github.event.pull_request.title }}, the E2E automation tests have been executed on the QE environment. Below is the summary:
" >> $GITHUB_ENV
+ if [ "${GITHUB_EVENT_NAME}" = "pull_request_review" ]; then
+ # Sanitize PR_TITLE to prevent environment variable injection
+ SANITIZED_PR_TITLE="$(echo "$PR_TITLE" | tr -d '\n' | tr -d '\r')"
+ echo "message=For the latest pull request titled $SANITIZED_PR_TITLE, the E2E automation tests have been executed on the QE environment. Below is the summary:
" >> $GITHUB_ENV
else
echo "message=This is to inform you that the scheduled E2E automation tests have been executed on the QE environment. Below is the summary:
" >> $GITHUB_ENV
fi
-
- name: Send Report via Email
if: ${{ success() || failure() }}
uses: dawidd6/action-send-mail@v3
@@ -262,7 +262,7 @@ jobs:
server_port: 587
username: ${{ secrets.QA_WORKFLOW_EMAIL }}
password: ${{ secrets.QA_WORKFLOW_EMAIL_PASSWORD }}
- subject: Test Automation Report - Next PMS
+ subject: Test Automation Report - Next CRM
html_body: |
@@ -400,6 +400,3 @@ jobs:
to: bots-erp-rtcamp-com-aaaajmbbbavnlkxccfuvoqjqsy@rtcamp.slack.com
from: no-reply@gmail.com
- - name: Cleanup
- if: ${{ always() }}
- uses: rtCamp/action-cleanup@master
diff --git a/tests/e2e/.env.example b/tests/e2e/.env.example
index ef7f07d2ae..319d303a35 100644
--- a/tests/e2e/.env.example
+++ b/tests/e2e/.env.example
@@ -23,4 +23,4 @@ EMP2_NAME=
EMP3_ID=
EMP3_EMAIL=
EMP3_PASS=
-EMP3_NAME=
\ No newline at end of file
+EMP3_NAME=
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index 7bfcac37f3..99f1242882 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -1,4 +1,346 @@
# Next-CRM Playwright E2E Test Automation Framework
+A comprehensive end-to-end test automation framework for Next-CRM built with Playwright, featuring data-driven testing, role-based authentication, and integrated Allure reporting.
+
## Table of Contents
+- [Overview](#overview)
+- [Framework Architecture](#framework-architecture)
+- [Prerequisites](#prerequisites)
+- [Installation](#installation)
+- [Configuration](#configuration)
+- [Project Structure](#project-structure)
+- [Running Tests](#running-tests)
+- [Naming Conventions](#naming-conventions)
+- [CI/CD Integration](#cicd-integration)
+- [Best Practices](#best-practices)
+- [Additional Resources](#additional-resources)
+
+---
+
+## Overview
+
+This framework provides a robust, scalable solution for testing Next-CRM application with the following key features:
+
+- **Playwright**: Fast, reliable end-to-end testing
+- **Page Object Model**: Maintainable and reusable page interactions
+- **Data-Driven Testing**: JSON-based test data management
+- **Role-Based Testing**: Support for multiple user roles (Admin, Manager, Employee)
+- **Parallel Execution**: Run tests concurrently for faster feedback
+- **Allure Reporting**: Rich, interactive test reports with history
+- **CI/CD Ready**: GitHub Actions integration with scheduled runs
+- **Authentication Management**: Persistent session storage per role/worker
+- **Global Setup/Teardown**: Automated test data creation and cleanup
+
+---
+
+## Framework Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Global Setup (Before Tests) │
+│ • Discovers active test cases │
+│ • Generates authentication states for each role │
+│ • Creates test data via API calls │
+│ • Stores data in JSON files │
+└─────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────┐
+│ Test Execution Phase │
+│ • Loads test data from JSON files │
+│ • Uses Page Objects for interactions │
+│ • Runs tests in parallel across workers │
+│ • Captures screenshots/videos on failure │
+│ • Generates Allure results │
+└─────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────┐
+│ Global Teardown (After Tests) │
+│ • Cleans up test data created during setup │
+│ • Deletes temporary files │
+└─────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────┐
+│ Report Generation │
+│ • Allure HTML report with history │
+│ • Playwright HTML report │
+│ • JSON results for analysis │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Prerequisites
+
+Before setting up the framework, ensure you have the following installed:
+
+- **Node.js**: v18 or higher ([Download](https://nodejs.org/))
+- **npm**: v8 or higher (comes with Node.js)
+- **Java JDK**: v17 or higher (for Allure reports) ([Download](https://www.oracle.com/java/technologies/downloads/))
+- **Allure Command Line**: For report generation
+ ```bash
+ npm install -g allure-commandline
+ ```
+
+---
+
+## Installation
+
+### 1. Clone the Repository
+
+```bash
+git clone
+cd next-crm/tests/e2e
+```
+
+### 2. Install Dependencies
+
+```bash
+npm ci
+```
+
+This will install:
+
+- Playwright Test
+- Playwright browsers
+- Allure Playwright reporter
+- dotenv for environment variables
+- Other utility dependencies
+
+### 3. Install Playwright Browsers
+
+```bash
+npx playwright install --with-deps
+```
+
+This installs Chromium, Firefox, and WebKit browsers along with system dependencies.
+
+---
+
+## Configuration
+
+### 1. Environment Variables
+
+Create a `.env` file in the `tests/e2e/` directory by copying the example:
+
+```bash
+cp .env.example .env
+```
+
+Update the `.env` file with your environment details.
+
+### 2. Playwright Configuration
+
+The `playwright.config.js` file contains all test execution settings.
+
+**Key Configuration Options:**
+
+- **fullyParallel**: Run all tests in parallel (faster execution)
+- **retries**: Retry flaky tests automatically
+- **workers**: Number of parallel test workers
+- **trace**: Capture detailed execution trace for debugging
+- **projects**: Define different test suites with different roles/browsers
+
+---
+
+## Project Structure
+
+```
+tests/e2e/
+├── .env.example # Environment variables template
+├── .gitignore # Git ignore rules
+├── README.md # This file
+├── package.json # Node.js dependencies and scripts
+├── playwright.config.js # Playwright test configuration
+├── playwright.fixture.js # Custom fixtures (auth, test data)
+│
+├── auth/ # Authentication state files
+│ ├── admin-API.json # API-only auth state
+│ └── admin-w0.json # Browser auth state for worker 0
+│
+├── data/ # Test data organized by role
+│ ├── admin/
+│ │ └── lead.js # Lead test data for admin
+│ ├── manager/ # Manager test data
+│ └── json-files/ # Generated JSON files (runtime)
+│ └── TC-LL-2.json # Test case specific data
+│
+├── globals/ # Global setup and teardown
+│ ├── globalSetup.js # Pre-test setup (auth, data creation)
+│ └── globalTeardown.js # Post-test cleanup
+│
+├── helpers/ # Helper functions
+│ ├── frappeRequests.js # Generic API request handlers
+│ ├── leadTabHelper.js # Lead-specific data operations
+│ └── storageStateHelper.js # Authentication state management
+│
+├── pageObjects/ # Page Object Model classes
+│ ├── leadsPage.js # Leads page interactions
+│ └── loginPage.js # Login page interactions
+│
+├── scripts/ # Utility scripts
+│ └── list-tests.js # Discovers active test cases
+│
+├── specs/ # Test specifications
+│ └── admin/
+│ └── lead.spec.js # Lead management test cases
+│
+├── utils/ # Utility functions
+│ ├── api/
+│ │ ├── authRequestForStorage.js # Authentication utilities
+│ │ ├── frappeRequests.js # API request wrappers
+│ │ └── leadRequests.js # Lead API operations
+│ ├── dataGenerators.js # Random test data generators
+│ ├── fileUtils.js # File operations (JSON read/write)
+│ └── stringUtils.js # String manipulation utilities
+│
+├── allure-results/ # Allure test results (generated)
+├── allure-report/ # Allure HTML report (generated)
+├── playwright-report/ # Playwright HTML report (generated)
+├── test-results/ # Screenshots, videos, traces (generated)
+└── results.json # JSON test results (generated)
+```
+
+---
+
+## Running Tests
+
+### Local Execution
+
+#### Run All Tests
+
+```bash
+cd tests/e2e/
+npm run e2e:tests
+```
+
+### Viewing Reports
+
+#### Allure Report (Recommended)
+
+Generate and open the interactive Allure report:
+
+```bash
+npm run allure:report
+npm run allure:serve
+```
+
+This will:
+
+1. Generate the Allure report from test results
+2. Start a local web server
+3. Open the report in your default browser
+
+#### Playwright HTML Report
+
+```bash
+npx playwright show-report
+```
+
+---
+
+## Naming Conventions
+
+- **Test IDs**: `TC--` (e.g., `TC-LL-2` for Lead List test case 2)
+ - `LL` = Lead List
+ - `LD` = Lead Detail
+ - `CO` = Contact
+ - `DE` = Deal
+- **Test Files**: `.spec.js` (e.g., `lead.spec.js`)
+- **Page Objects**: `Page.js` (e.g., `leadsPage.js`)
+- **Helpers**: `Helper.js` (e.g., `leadTabHelper.js`)
+
+---
+
+## CI/CD Integration
+
+### GitHub Actions Workflow
+
+The framework includes a complete CI/CD pipeline (`.github/workflows/e2e-playwright-test.yml`).
+
+### Running Tests on CI
+
+Tests automatically run when:
+
+- A PR is approved
+- Daily at the scheduled time
+- Manually triggered from GitHub Actions
+
+---
+
+## Best Practices
+
+### 1. Test Independence
+
+- Each test should be independent and self-contained
+- Don't rely on test execution order
+- Create necessary data in test setup, clean up in teardown
+
+### 2. Stable Locators
+
+Prefer stable locators:
+
+```javascript
+// Good - Data attributes
+page.locator('[data-testid="create-button"]');
+
+// Good - Role-based
+page.getByRole("button", { name: "Create" });
+
+// Avoid - CSS classes (can change)
+page.locator(".btn-primary");
+```
+
+### 3. Explicit Waits
+
+Always use explicit waits:
+
+```javascript
+// Wait for element to be visible
+await expect(page.locator("#element")).toBeVisible();
+
+// Wait for specific state
+await page.waitForLoadState("networkidle");
+```
+
+### 4. Error Handling
+
+Add meaningful error messages:
+
+```javascript
+expect(result, "Lead should be created successfully").toBe(true);
+```
+
+### 5. Code Reusability
+
+- Use Page Objects for reusable interactions
+- Extract common utilities to helper functions
+- Share test data across similar tests
+
+### 6. Naming Conventions
+
+- Use descriptive test names
+- Follow consistent naming patterns
+- Include test case ID in test name
+
+### 7. Documentation
+
+- Add comments for complex logic
+- Document custom fixtures and helpers
+- Keep README up to date
+
+---
+
+## Additional Resources
+
+- [Playwright Documentation](https://playwright.dev/docs/intro)
+- [Allure Report Documentation](https://docs.qameta.io/allure/)
+- [JavaScript Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices)
+
+---
+
+## Support
+
+For questions or issues:
+
+- Contact the QA team
diff --git a/tests/e2e/data/admin/lead.js b/tests/e2e/data/admin/lead.js
index 2d5f84b690..ee8d7a3cc5 100644
--- a/tests/e2e/data/admin/lead.js
+++ b/tests/e2e/data/admin/lead.js
@@ -4,4 +4,4 @@ export default {
"TC-LL-2": {
infoPayloadCreateLead: generateLeadData(),
},
-};
\ No newline at end of file
+};
diff --git a/tests/e2e/globals/globalSetup.js b/tests/e2e/globals/globalSetup.js
index 014cf685af..e486ccdf21 100644
--- a/tests/e2e/globals/globalSetup.js
+++ b/tests/e2e/globals/globalSetup.js
@@ -3,9 +3,12 @@ import { createJSONFile, populateJsonStubs } from "../utils/fileUtils";
import path from "path";
import fs from "fs";
import { execSync } from "child_process";
-import { create } from "domain";
+import { fileURLToPath } from "url";
import { createLeadForTestCases } from "../helpers/leadTabHelper";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
const globalSetup = async () => {
console.log("🚀 Starting global setup...");
@@ -79,7 +82,6 @@ const globalSetup = async () => {
for (const tcId of allTCIds) {
console.log(`➡️ Processing ${tcId}`);
await createLeadForTestCases([tcId], jsonDir);
-
}
console.log("✅ Data generation completed for all TC IDs! Global setup done.");
diff --git a/tests/e2e/globals/globalTeardown.js b/tests/e2e/globals/globalTeardown.js
index 338c262c2e..f437f2ea5a 100644
--- a/tests/e2e/globals/globalTeardown.js
+++ b/tests/e2e/globals/globalTeardown.js
@@ -1,8 +1,12 @@
import path from "path";
import fs from "fs";
+import { fileURLToPath } from "url";
import { deleteLeadForTestCases } from "../helpers/leadTabHelper.js";
// ------------------------------------------------------------------------------------------
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
/**
* Global teardown function to delete the stale test data after running tests.
*/
@@ -25,7 +29,6 @@ const globalTeardown = async () => {
await deleteLeadForTestCases([tcId], path.join(__dirname, "../data/json-files"));
}
console.log("✅ Global teardown completed: test data cleaned up.");
-
};
export default globalTeardown;
diff --git a/tests/e2e/helpers/frappeRequests.js b/tests/e2e/helpers/frappeRequests.js
deleted file mode 100644
index f73db2346a..0000000000
--- a/tests/e2e/helpers/frappeRequests.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { request } from "@playwright/test";
-import path from "path";
-import fs from "fs";
-import config from "../../playwright.config";
-
-// Load config variables
-const baseURL = config.use?.baseURL;
-// ------------------------------------------------------------------------------------------
-
-/**
- * Helper function to ensure storage state is loaded for respective roles.
- */
-const loadAuthState = (role) => {
- const filePath = path.resolve(__dirname, `../../auth/${role}-API.json`);
- if (!fs.existsSync(filePath)) {
- throw new Error(`Auth state file for ${role} not found: ${filePath}`);
- }
- return filePath;
-};
-// ------------------------------------------------------------------------------------------
-
-/**
- * Helper function to build and execute an API request.
- * Supports JSON (data) or form-encoded (form) payloads.
- */
-export const apiRequest = async (endpoint, options = {}, role = "admin") => {
- const authFilePath = loadAuthState(role);
- const requestContext = await request.newContext({ baseURL, storageState: authFilePath });
-
- // Determine payload and headers
- let body;
- const headers = { ...(options.headers || {}) };
-
- if (options.form) {
- // form-encoded
- body = new URLSearchParams(options.form).toString();
- headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
- } else if (options.data) {
- // JSON body
- body = JSON.stringify(options.data);
- headers["Content-Type"] = "application/json";
- }
-
- const fetchOptions = {
- method: options.method || (body ? "POST" : "GET"),
- headers,
- };
- if (body) {
- fetchOptions.data = body;
- }
-
- const response = await requestContext.fetch(endpoint, fetchOptions);
-
- if (!response.ok()) {
- const text = await response.text();
- await requestContext.dispose();
- throw new Error(
- `API request failed for ${role} and endpoint ${endpoint}: ${response.status()} ${response.statusText()}\n${text}`
- );
- }
-
- // Playwright's response.headers() returns a plain object
- const headersObj = response.headers();
- const contentType = headersObj["content-type"] || "";
- let responseData;
-
- if (contentType.includes("application/json")) {
- responseData = await response.json();
- } else {
- responseData = await response.text();
- }
-
- await requestContext.dispose();
- return responseData;
-};
-// ------------------------------------------------------------------------------------------
-
-/**
- * Filter the Results via Frappe reportview API
- */
-export const filterApi = async (docType, filters, role = "admin") => {
- const endpoint = "/api/method/frappe.desk.reportview.get";
-
- // Frappe expects form-encoded doctype + JSON string filters
- const formPayload = {
- doctype: docType,
- filters: JSON.stringify(filters),
- };
-
- return apiRequest(
- endpoint,
- {
- method: "POST",
- form: formPayload,
- },
- role
- );
-};
-// ------------------------------------------------------------------------------------------
-
diff --git a/tests/e2e/helpers/leadTabHelper.js b/tests/e2e/helpers/leadTabHelper.js
index 8ad6b813b1..bfe6810b76 100644
--- a/tests/e2e/helpers/leadTabHelper.js
+++ b/tests/e2e/helpers/leadTabHelper.js
@@ -5,76 +5,79 @@ import { createLead, deleteLead } from "../utils/api/leadRequests";
import { filterApi } from "../utils/api/frappeRequests.js";
// ------------------------------------------------------------------------------------------
-
/**
- * Creates Lead(s) for provided testCaseID(s).
- * @param {string[]} testCaseIDs IDs to process
- * @param {string} jsonDir Directory where JSON stubs are located
+ * Creates Lead(s) for provided testCaseID(s).
+ * @param {string[]} testCaseID IDs to process
+ * @param {string} jsonDir Directory where JSON stubs are located
*/
-export const createLeadForTestCases = async (testCaseIDs, jsonDir) => {
- if (!Array.isArray(testCaseIDs) || testCaseIDs.length === 0) return;
- const [tcId] = testCaseIDs;
- const stubPath = path.join(jsonDir, `${tcId}.json`);
- const fullStub = await readJSONFile(stubPath);
- const entry = fullStub[tcId];
- if (!entry || !entry.payloadCreateLead) {
- console.warn(`Skipping ${tcId}: no payloadCreateLead`);
- return;
- }
+export const createLeadForTestCases = async (testCaseID, jsonDir) => {
+ if (!Array.isArray(testCaseID) || testCaseID.length === 0) return;
+ const [tcId] = testCaseID;
+ const stubPath = path.join(jsonDir, `${tcId}.json`);
+ const fullStub = await readJSONFile(stubPath);
+ const entry = fullStub[tcId];
+ if (!entry || !entry.payloadCreateLead) {
+ console.warn(`Skipping ${tcId}: no payloadCreateLead`);
+ return;
+ }
- try {
- await createLead(entry.payloadCreateLead);
- console.log(`✅ Created Lead for '${tcId}'`);
- } catch (error) {
- console.error(`❌ Failed to create Lead for '${tcId}': ${error.message}`);
- }
+ try {
+ await createLead(entry.payloadCreateLead);
+ console.log(`✅ Created Lead for '${tcId}'`);
+ } catch (error) {
+ console.error(`❌ Failed to create Lead for '${tcId}': ${error.message}`);
+ }
};
// ------------------------------------------------------------------------------------------
/**
* Deletes Lead(s) for provided testCaseID(s).
- * @param {string[]} testCaseIDs IDs to process
- * @param {string} jsonDir Directory where JSON stubs are located
+ * @param {string[]} testCaseID IDs to process
+ * @param {string} jsonDir Directory where JSON stubs are located
*/
-export const deleteLeadForTestCases = async (testCaseIDs, jsonDir) => {
- if (!Array.isArray(testCaseIDs) || testCaseIDs.length === 0) return;
- const [tcId] = testCaseIDs;
- const stubPath = path.join(jsonDir, `${tcId}.json`);
- const fullStub = await readJSONFile(stubPath);
- const entry = fullStub[tcId];
- //Print entry for debugging
- console.log(`Entry or TEST CASE for ${tcId}:`, entry);
- if (!entry || !entry.infoPayloadCreateLead || !entry.infoPayloadCreateLead.company_name) {
- console.warn(`Skipping ${tcId}: No company_name to identify Lead`);
- return;
- }
-
- try {
- const leadRes = await filterApi("Lead", [["Lead", "company_name", "=", entry.infoPayloadCreateLead.company_name]]);
+export const deleteLeadForTestCases = async (testCaseID, jsonDir) => {
+ if (!Array.isArray(testCaseID) || testCaseID.length === 0) return;
+ const [tcId] = testCaseID;
+ const stubPath = path.join(jsonDir, `${tcId}.json`);
+ const fullStub = await readJSONFile(stubPath);
+ const entry = fullStub[tcId];
+ //Print entry for debugging
+ console.log(`Entry or TEST CASE for ${tcId}:`, entry);
+ if (!entry || !entry.infoPayloadCreateLead || !entry.infoPayloadCreateLead.company_name) {
+ console.warn(`Skipping ${tcId}: No company_name to identify Lead`);
+ return;
+ }
- // Extract lead names from the response and delete each lead
- if (leadRes?.message?.values && Array.isArray(leadRes.message.values)) {
- const leadNames = leadRes.message.values.map(valueArray => valueArray[0]);
+ try {
+ const leadRes = await filterApi("Lead", [["Lead", "company_name", "=", entry.infoPayloadCreateLead.company_name]]);
- // Delete each lead individually
- const deletePromises = leadNames.map(async (leadName) => {
- try {
- await deleteLead(leadName);
- console.log(`✅ Successfully deleted Lead: ${leadName}`);
- } catch (deleteError) {
- console.error(`❌ Failed to delete Lead '${leadName}': ${deleteError.message}`);
- throw deleteError;
- }
- });
+ // Extract lead names from the response and delete each lead
+ if (leadRes?.message?.values && Array.isArray(leadRes.message.values)) {
+ const leadNames = leadRes.message.values.map((valueArray) => valueArray[0]);
- // Wait for all deletions to complete
- await Promise.all(deletePromises);
- console.log(`✅ Completed deletion process for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}'`);
- } else {
- console.warn(`No leads found for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}'`);
+ // Delete each lead individually
+ const deletePromises = leadNames.map(async (leadName) => {
+ try {
+ await deleteLead(leadName);
+ console.log(`✅ Successfully deleted Lead: ${leadName}`);
+ } catch (deleteError) {
+ console.error(`❌ Failed to delete Lead '${leadName}': ${deleteError.message}`);
+ throw deleteError;
}
- } catch (error) {
- console.error(`❌ Failed to delete Lead for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}': ${error.message}`);
+ });
+
+ // Wait for all deletions to complete
+ await Promise.all(deletePromises);
+ console.log(
+ `✅ Completed deletion process for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}'`,
+ );
+ } else {
+ console.warn(`No leads found for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}'`);
}
+ } catch (error) {
+ console.error(
+ `❌ Failed to delete Lead for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}': ${error.message}`,
+ );
+ }
};
diff --git a/tests/e2e/helpers/storageStateHelper.js b/tests/e2e/helpers/storageStateHelper.js
index e32b9f7421..323be3e474 100644
--- a/tests/e2e/helpers/storageStateHelper.js
+++ b/tests/e2e/helpers/storageStateHelper.js
@@ -1,10 +1,14 @@
// storageStateHelper.js
import path from "path";
+import { fileURLToPath } from "url";
import { request, chromium } from "@playwright/test";
import config from "../playwright.config";
import { loginIntoNextPMS } from "../utils/api/authRequestForStorage";
import fs from "fs";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
// Base URL from Playwright config
const baseURL = config.use?.baseURL;
diff --git a/tests/e2e/package.json b/tests/e2e/package.json
index 35829d907c..3e0ed12642 100644
--- a/tests/e2e/package.json
+++ b/tests/e2e/package.json
@@ -1,9 +1,10 @@
{
"name": "e2e",
"version": "1.0.0",
+ "type": "module",
"main": "index.js",
"scripts": {
- "e2e:tests": "npx playwright test --config=tests/e2e/playwright.config.js",
+ "e2e:tests": "npx playwright test --config=playwright.config.js",
"allure:report": "allure generate allure-results --clean -o allure-report-for-count-extraction",
"allure:serve": "allure serve allure-results",
"allure:htmlReport": "allure generate allure-results --single-file --clean -o allure-report",
diff --git a/tests/e2e/pageObjects/leadsPage.js b/tests/e2e/pageObjects/leadsPage.js
index 76df763169..ad6e7faaea 100644
--- a/tests/e2e/pageObjects/leadsPage.js
+++ b/tests/e2e/pageObjects/leadsPage.js
@@ -4,111 +4,111 @@ import { expect } from "@playwright/test";
* LeadsPage class handles interactions with the leads Tab.
*/
export class LeadsPage {
- /**
- * Initializes the LeadsPage object.
- * @param {import('@playwright/test').Page} page - Playwright page instance.
- */
- constructor(page) {
- this.page = page;
-
- // Create Lead Dialog Selectors
- this.createLeadBtn = page.getByRole('button', { name: 'Create' });
- this.createLeadHeading = page.getByRole('heading', { name: 'Create Lead' });
- this.closeLeadDialogBtn = page.getByRole('button').filter({ hasText: /^$/ }).nth(1);
- this.salutationBtn = page.getByRole('button', { name: 'Select Salutation' });
- this.SalutationDropDownVal = (Salutation) => page.getByRole('option', { name: `${Salutation}`, exact: true });
- this.firstNameInput = page.getByRole('textbox', { name: 'Enter First Name' });
- this.lastNameInput = page.getByRole('textbox', { name: 'Enter Last Name' });
- this.emailInput = page.getByRole('textbox', { name: 'Enter Email' });
- this.mobileInput = page.getByRole('textbox', { name: 'Enter Mobile No' });
- this.genderBtn = page.getByRole('button', { name: 'Select Gender' });
- this.genderDropDownVal = (gender) => page.getByRole('option', { name: `${gender}`, exact: true });
- this.organizationNameInput = page.getByRole('textbox', { name: 'Enter Organization Name' });
- this.annualRevenueInput = page.getByRole('textbox', { name: 'Enter Annual Revenue' });
- this.selectLeadStatus = (LeadValue) => page.locator('select').last().selectOption({ value: `${LeadValue}` });
- this.createButton = page.getByRole('button', { name: 'Create' });
-
- //Leads Side bar
- //Person section
- this.LeadFirstName = page.getByRole('textbox', { name: 'Add First Name...' });
- this.LeadLastName = page.getByRole('textbox', { name: 'Add Last Name...' });
- this.LeadEmail = page.getByRole('textbox', { name: 'Add Email...' });
- this.LeadMobile = page.getByRole('textbox', { name: 'Add Mobile No...' });
- //this.LeadGender = page.getByRole('button', { name: 'Select Gender' });
- this.LeadOrganization = page.getByRole('textbox', { name: 'Add Organization Name...' });
- this.LeadAnnualRevenue = page.getByRole('textbox', { name: 'Add Annual Revenue...' });
-
- //Lead -> Top Bar
- this.LeadStatusInPage = (LeadValue) => page.getByRole('button', { name: `${LeadValue}`, exact: true });
- }
-
- // --------------------------------------
- // General
- // --------------------------------------
-
- /**
- * Navigates to the timesheet page and waits for it to fully load.
- */
- async goto() {
- await this.page.goto("/next-crm/leads/view", { waitUntil: "domcontentloaded" });
- }
- // ------------------------------------------------------------------------------------------
-
-
- // --------------------------------------
- // Leads Tab Actions
- // --------------------------------------
-
-
- /**
- * Creates a new Lead with provided test case data
- * @param {Object} tcData Test case data containing lead details
- */
- async createLead(tcData) {
- // Click on 'Create Lead' button
- await this.createLeadBtn.click();
-
- // Verify 'Create Lead' dialog is opened
- await expect(this.createLeadHeading).toBeVisible();
-
- // Fill Lead details
- await this.salutationBtn.click();
- await this.SalutationDropDownVal(tcData.salutation).click();
- await this.firstNameInput.fill(tcData.first_name);
- await this.lastNameInput.fill(tcData.last_name);
- await this.emailInput.fill(tcData.email_id);
- await this.mobileInput.fill(tcData.mobile_no);
- await this.genderBtn.click();
- await this.genderDropDownVal(tcData.gender).click();
- await this.organizationNameInput.fill(tcData.company_name);
- await this.annualRevenueInput.fill(tcData.annual_revenue);
- await this.selectLeadStatus(tcData.status);
-
-
- // Click on 'Create' button to create Lead
- await this.createButton.click();
-
- // Verify Lead is created and dialog is closed
- await expect(this.createLeadHeading).toBeHidden();
-
- //Wait until lead first name is visible in side bar
- await expect(this.LeadFirstName).toBeVisible();
-
- }
- // ------------------------------------------------------------------------------------------
- /**
- * Verify the lead details in side bar
- * * @param {Object} tcData Test case data containing lead details
- * @returns {boolean} true if all verifications are passed
- */
- async verifyLeadDetailsInSideBar(tcData) {
- //Verify the lead details in side bar
- await expect(this.LeadFirstName).toHaveValue(tcData.first_name);
- await expect(this.LeadLastName).toHaveValue(tcData.last_name);
- await expect(this.LeadEmail).toHaveValue(tcData.email_id);
- await expect(this.LeadMobile).toHaveValue(tcData.mobile_no);
- //Return true if all the verifications are passed
- return true;
- }
- // ------------------------------------------------------------------------------------------
+ /**
+ * Initializes the LeadsPage object.
+ * @param {import('@playwright/test').Page} page - Playwright page instance.
+ */
+ constructor(page) {
+ this.page = page;
+
+ // Create Lead Dialog Selectors
+ this.createLeadBtn = page.getByRole("button", { name: "Create" });
+ this.createLeadHeading = page.getByRole("heading", { name: "Create Lead" });
+ this.closeLeadDialogBtn = page.getByRole("button").filter({ hasText: /^$/ }).nth(1);
+ this.salutationBtn = page.getByRole("button", { name: "Select Salutation" });
+ this.SalutationDropDownVal = (Salutation) => page.getByRole("option", { name: `${Salutation}`, exact: true });
+ this.firstNameInput = page.getByRole("textbox", { name: "Enter First Name" });
+ this.lastNameInput = page.getByRole("textbox", { name: "Enter Last Name" });
+ this.emailInput = page.getByRole("textbox", { name: "Enter Email" });
+ this.mobileInput = page.getByRole("textbox", { name: "Enter Mobile No" });
+ this.genderBtn = page.getByRole("button", { name: "Select Gender" });
+ this.genderDropDownVal = (gender) => page.getByRole("option", { name: `${gender}`, exact: true });
+ this.organizationNameInput = page.getByRole("textbox", { name: "Enter Organization Name" });
+ this.annualRevenueInput = page.getByRole("textbox", { name: "Enter Annual Revenue" });
+ this.selectLeadStatus = (LeadValue) =>
+ page
+ .locator("select")
+ .last()
+ .selectOption({ value: `${LeadValue}` });
+ this.createButton = page.getByRole("button", { name: "Create" });
+
+ //Leads Side bar
+ //Person section
+ this.LeadFirstName = page.getByRole("textbox", { name: "Add First Name..." });
+ this.LeadLastName = page.getByRole("textbox", { name: "Add Last Name..." });
+ this.LeadEmail = page.getByRole("textbox", { name: "Add Email..." });
+ this.LeadMobile = page.getByRole("textbox", { name: "Add Mobile No..." });
+ //this.LeadGender = page.getByRole('button', { name: 'Select Gender' });
+ this.LeadOrganization = page.getByRole("textbox", { name: "Add Organization Name..." });
+ this.LeadAnnualRevenue = page.getByRole("textbox", { name: "Add Annual Revenue..." });
+
+ //Lead -> Top Bar
+ this.LeadStatusInPage = (LeadValue) => page.getByRole("button", { name: `${LeadValue}`, exact: true });
+ }
+
+ // --------------------------------------
+ // General
+ // --------------------------------------
+
+ /**
+ * Navigates to the timesheet page and waits for it to fully load.
+ */
+ async goto() {
+ await this.page.goto("/next-crm/leads/view", { waitUntil: "domcontentloaded" });
+ }
+ // ------------------------------------------------------------------------------------------
+
+ // --------------------------------------
+ // Leads Tab Actions
+ // --------------------------------------
+
+ /**
+ * Creates a new Lead with provided test case data
+ * @param {Object} tcData Test case data containing lead details
+ */
+ async createLead(tcData) {
+ // Click on 'Create Lead' button
+ await this.createLeadBtn.click();
+
+ // Verify 'Create Lead' dialog is opened
+ await expect(this.createLeadHeading).toBeVisible();
+
+ // Fill Lead details
+ await this.salutationBtn.click();
+ await this.SalutationDropDownVal(tcData.salutation).click();
+ await this.firstNameInput.fill(tcData.first_name);
+ await this.lastNameInput.fill(tcData.last_name);
+ await this.emailInput.fill(tcData.email_id);
+ await this.mobileInput.fill(tcData.mobile_no);
+ await this.genderBtn.click();
+ await this.genderDropDownVal(tcData.gender).click();
+ await this.organizationNameInput.fill(tcData.company_name);
+ await this.annualRevenueInput.fill(tcData.annual_revenue);
+ await this.selectLeadStatus(tcData.status);
+
+ // Click on 'Create' button to create Lead
+ await this.createButton.click();
+
+ // Verify Lead is created and dialog is closed
+ await expect(this.createLeadHeading).toBeHidden();
+
+ //Wait until lead first name is visible in side bar
+ await expect(this.LeadFirstName).toBeVisible();
+ }
+ // ------------------------------------------------------------------------------------------
+ /**
+ * Verify the lead details in side bar
+ * @param {Object} tcData Test case data containing lead details
+ * @returns {boolean} true if all verifications are passed
+ */
+ async verifyLeadDetailsInSideBar(tcData) {
+ //Verify the lead details in side bar
+ await expect(this.LeadFirstName).toHaveValue(tcData.first_name);
+ await expect(this.LeadLastName).toHaveValue(tcData.last_name);
+ await expect(this.LeadEmail).toHaveValue(tcData.email_id);
+ await expect(this.LeadMobile).toHaveValue(tcData.mobile_no);
+ //Return true if all the verifications are passed
+ return true;
+ }
+ // ------------------------------------------------------------------------------------------
}
diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js
index 536ade025b..4128d3c2e5 100644
--- a/tests/e2e/playwright.config.js
+++ b/tests/e2e/playwright.config.js
@@ -1,16 +1,19 @@
// @ts-check
-const { defineConfig, devices } = require("@playwright/test");
+import { defineConfig, devices } from "@playwright/test";
import path from "path";
-import dotenv from 'dotenv';
+import dotenv from "dotenv";
+import { fileURLToPath } from "url";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
// Load environment variables from .env in this folder
-dotenv.config({ path: './.env' });
+dotenv.config({ path: "./.env" });
/**
* @see https://playwright.dev/docs/test-configuration
*/
-module.exports = defineConfig({
+export default defineConfig({
/* Global setup file */
globalSetup: "./globals/globalSetup.js",
@@ -36,7 +39,7 @@ module.exports = defineConfig({
workers: process.env.CI ? 7 : undefined, // Use 7 workers on CI, defaults to the number of CPU cores otherwise
// Limit the number of failures on CI to save resources
- maxFailures: process.env.CI ? 5 : undefined, // Max faliures on CI = 5, no limit locally
+ maxFailures: process.env.CI ? 5 : undefined, // Max failures on CI = 5, no limit locally
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
@@ -79,6 +82,5 @@ module.exports = defineConfig({
use: { ...devices["Desktop Chrome"] },
metadata: { TEST_ROLE: "admin" },
},
-
],
});
diff --git a/tests/e2e/playwright.fixture.cjs b/tests/e2e/playwright.fixture.js
similarity index 78%
rename from tests/e2e/playwright.fixture.cjs
rename to tests/e2e/playwright.fixture.js
index a544720bed..0f24d1063e 100644
--- a/tests/e2e/playwright.fixture.cjs
+++ b/tests/e2e/playwright.fixture.js
@@ -1,7 +1,11 @@
-const { test: base, expect, devices } = require("@playwright/test");
-const path = require("path");
-const fs = require("fs").promises;
-const { storeStorageState } = require("./helpers/storageStateHelper");
+import { test as base, expect, devices } from "@playwright/test";
+import path from "path";
+import fs from "fs/promises";
+import { fileURLToPath } from "url";
+import { storeStorageState } from "./helpers/storageStateHelper.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
// Define the single shared JSON directory
const SHARED_JSON_DIR = path.resolve(__dirname, "data", "json-files");
@@ -10,7 +14,7 @@ const SHARED_JSON_DIR = path.resolve(__dirname, "data", "json-files");
const test = base.extend({
// Worker-scoped fixture: generate a unique storageState per role per worker
authState: [
- async ({ }, use, testInfo) => {
+ async ({}, use, testInfo) => {
const role = testInfo.project.metadata.TEST_ROLE;
if (!role) {
throw new Error("`metadata.TEST_ROLE` must be set on the project");
@@ -38,7 +42,7 @@ const test = base.extend({
// Fixed JSON directory - same for all workers
jsonDir: [
- async ({ }, use) => {
+ async ({}, use) => {
//console.log(`📁 Using shared JSON directory: ${SHARED_JSON_DIR}`);
// Verify directory exists (should be created in global setup)
@@ -47,9 +51,7 @@ const test = base.extend({
//console.log("✅ JSON directory exists");
} catch {
console.error("❌ JSON directory missing - was global setup run?");
- throw new Error(
- "JSON directory not found. Ensure global setup has run."
- );
+ throw new Error("JSON directory not found. Ensure global setup has run.");
}
await use(SHARED_JSON_DIR);
@@ -63,4 +65,4 @@ const test = base.extend({
},
});
-module.exports = { test, expect, devices };
+export { test, expect, devices };
diff --git a/tests/e2e/scripts/list-tests.js b/tests/e2e/scripts/list-tests.js
index 3a56abb432..e8e480c02f 100644
--- a/tests/e2e/scripts/list-tests.js
+++ b/tests/e2e/scripts/list-tests.js
@@ -1,9 +1,14 @@
#!/usr/bin/env node
-const fs = require("fs");
-const path = require("path");
-const parser = require("@babel/parser");
-const traverse = require("@babel/traverse").default;
+import fs from "fs";
+import path from "path";
+import parser from "@babel/parser";
+import traverseModule from "@babel/traverse";
+import { fileURLToPath } from "url";
+
+const traverse = traverseModule.default || traverseModule;
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
// Point to your specs directory
const TEST_DIR = path.resolve(__dirname, "../specs");
@@ -91,7 +96,7 @@ function collectTests(rootDir) {
const isSkipped = ["skip", "fixme"].includes(kind) || inSkipDesc;
results.push(
- new TestNode(path.relative(process.cwd(), filePath), titleArg.loc.start.line, fullTitle, isOnly, isSkipped)
+ new TestNode(path.relative(process.cwd(), filePath), titleArg.loc.start.line, fullTitle, isOnly, isSkipped),
);
},
exit(pathNode) {
diff --git a/tests/e2e/specs/admin/lead.spec.js b/tests/e2e/specs/admin/lead.spec.js
index 9becbb8184..6206a09b27 100644
--- a/tests/e2e/specs/admin/lead.spec.js
+++ b/tests/e2e/specs/admin/lead.spec.js
@@ -1,4 +1,4 @@
-import { test, expect } from "../../playwright.fixture.cjs";
+import { test, expect } from "../../playwright.fixture.js";
import { LeadsPage } from "../../pageObjects/leadsPage.js";
import * as allure from "allure-js-commons";
import { readJSONFile } from "../../utils/fileUtils.js";
@@ -6,37 +6,34 @@ import path from "path";
// Test suite for Lead creation and verification
test.describe("Lead Management", () => {
- let leadsPage;
- let testData;
+ let leadsPage;
+ let testData;
- // Setup: Initialize page objects and load test data
- test.beforeEach(async ({ page, jsonDir }) => {
- leadsPage = new LeadsPage(page);
+ // Setup: Initialize page objects and load test data
+ test.beforeEach(async ({ page, jsonDir }) => {
+ leadsPage = new LeadsPage(page);
- // Navigate to leads page
- await leadsPage.goto();
- });
+ // Navigate to leads page
+ await leadsPage.goto();
+ });
- test("TC-LL-2: Create a new lead and verify details", async ({ jsonDir }) => {
- allure.story("Lead");
+ test("TC-LL-2: Create a new lead and verify details", async ({ jsonDir }) => {
+ allure.story("Lead");
- // Read test data from JSON file generated in global setup
- const tcId = "TC-LL-2";
- const dataPath = path.join(jsonDir, `${tcId}.json`);
- const jsonData = await readJSONFile(dataPath);
- testData = jsonData[tcId].infoPayloadCreateLead;
+ // Read test data from JSON file generated in global setup
+ const tcId = "TC-LL-2";
+ const dataPath = path.join(jsonDir, `${tcId}.json`);
+ const jsonData = await readJSONFile(dataPath);
+ testData = jsonData[tcId].infoPayloadCreateLead;
+ // Step 1: Create a new lead with test data
+ await leadsPage.createLead(testData);
- // Step 1: Create a new lead with test data
- await leadsPage.createLead(testData);
+ // Step 2: Verify lead is created and sidebar is visible
+ await expect(leadsPage.LeadFirstName).toBeVisible();
- // Step 2: Verify lead is created and sidebar is visible
- await expect(leadsPage.LeadFirstName).toBeVisible();
-
- // Step 3: Verify lead details in the sidebar
- const isVerified = await leadsPage.verifyLeadDetailsInSideBar(testData);
- expect(isVerified).toBe(true);
-
-
- });
+ // Step 3: Verify lead details in the sidebar
+ const isVerified = await leadsPage.verifyLeadDetailsInSideBar(testData);
+ expect(isVerified).toBe(true);
+ });
});
diff --git a/tests/e2e/utils/api/frappeRequests.js b/tests/e2e/utils/api/frappeRequests.js
index f73db2346a..669a2a6d82 100644
--- a/tests/e2e/utils/api/frappeRequests.js
+++ b/tests/e2e/utils/api/frappeRequests.js
@@ -1,8 +1,12 @@
import { request } from "@playwright/test";
import path from "path";
+import { fileURLToPath } from "url";
import fs from "fs";
import config from "../../playwright.config";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
// Load config variables
const baseURL = config.use?.baseURL;
// ------------------------------------------------------------------------------------------
@@ -55,7 +59,7 @@ export const apiRequest = async (endpoint, options = {}, role = "admin") => {
const text = await response.text();
await requestContext.dispose();
throw new Error(
- `API request failed for ${role} and endpoint ${endpoint}: ${response.status()} ${response.statusText()}\n${text}`
+ `API request failed for ${role} and endpoint ${endpoint}: ${response.status()} ${response.statusText()}\n${text}`,
);
}
@@ -93,8 +97,7 @@ export const filterApi = async (docType, filters, role = "admin") => {
method: "POST",
form: formPayload,
},
- role
+ role,
);
};
// ------------------------------------------------------------------------------------------
-
diff --git a/tests/e2e/utils/api/leadRequests.js b/tests/e2e/utils/api/leadRequests.js
index 2da56cb9db..b2b0a8d669 100644
--- a/tests/e2e/utils/api/leadRequests.js
+++ b/tests/e2e/utils/api/leadRequests.js
@@ -1,6 +1,7 @@
import { request } from "@playwright/test";
import path from "path";
import fs from "fs";
+import { fileURLToPath } from "url";
import config from "../../playwright.config";
// Load config variables
@@ -11,54 +12,55 @@ const baseURL = config.use?.baseURL;
* Helper function to ensure storage state is loaded for respective roles.
*/
const loadAuthState = (role) => {
- const filePath = path.resolve(__dirname, `../../auth/${role}-API.json`);
- if (!fs.existsSync(filePath)) {
- throw new Error(`Auth state file for ${role} not found: ${filePath}`);
- }
- return filePath;
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = path.dirname(__filename);
+ const filePath = path.resolve(__dirname, `../../auth/${role}-API.json`);
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`Auth state file for ${role} not found: ${filePath}`);
+ }
+ return filePath;
};
// ------------------------------------------------------------------------------------------
/**
- * Helper function to load build the API request
+ * Helper function to build the API request
*/
export const apiRequest = async (endpoint, options = {}, role = "admin") => {
- const authFilePath = loadAuthState(role);
- const requestContext = await request.newContext({ baseURL, storageState: authFilePath });
- const response = await requestContext.fetch(endpoint, {
- ...options,
- postData: options.data ? JSON.stringify(options.data) : undefined, // Transform to json format
- headers: {
- "Content-Type": "application/json",
- ...(options.headers || {}),
- },
- });
-
- let responseData;
- if (response.ok()) {
- responseData = await response.json();
- } else {
- await requestContext.dispose();
- throw new Error(
- `API request failed for ${role} and endpoint ${endpoint}: ${response.status()} ${response.statusText()}`
- );
- }
+ const authFilePath = loadAuthState(role);
+ const requestContext = await request.newContext({ baseURL, storageState: authFilePath });
+ const response = await requestContext.fetch(endpoint, {
+ ...options,
+ postData: options.data ? JSON.stringify(options.data) : undefined, // Transform to json format
+ headers: {
+ "Content-Type": "application/json",
+ ...(options.headers || {}),
+ },
+ });
+ let responseData;
+ if (response.ok()) {
+ responseData = await response.json();
+ } else {
await requestContext.dispose();
- return responseData;
+ throw new Error(
+ `API request failed for ${role} and endpoint ${endpoint}: ${response.status()} ${response.statusText()}`,
+ );
+ }
+
+ await requestContext.dispose();
+ return responseData;
};
// ------------------------------------------------------------------------------------------
-
/**
* Get Lead List
*/
export const getLead = async (leadName, role = "admin") => {
- const endpoint = `/api/resource/Lead/${encodeURIComponent(leadName)}`;
- const options = {
- method: "GET",
- };
- return await apiRequest(endpoint, options, role);
+ const endpoint = `/api/resource/Lead/${encodeURIComponent(leadName)}`;
+ const options = {
+ method: "GET",
+ };
+ return await apiRequest(endpoint, options, role);
};
// ------------------------------------------------------------------------------------------
@@ -66,12 +68,12 @@ export const getLead = async (leadName, role = "admin") => {
* Create Lead via API
*/
export const createLead = async (leadData, role = "admin") => {
- const endpoint = `/api/resource/Lead`;
- const options = {
- method: "POST",
- data: leadData,
- };
- return await apiRequest(endpoint, options, role);
+ const endpoint = `/api/resource/Lead`;
+ const options = {
+ method: "POST",
+ data: leadData,
+ };
+ return await apiRequest(endpoint, options, role);
};
// ------------------------------------------------------------------------------------------
@@ -79,10 +81,10 @@ export const createLead = async (leadData, role = "admin") => {
* Delete Lead via API
*/
export const deleteLead = async (leadName, role = "admin") => {
- const endpoint = `/api/resource/Lead/${encodeURIComponent(leadName)}`;
- const options = {
- method: "DELETE",
- };
- return await apiRequest(endpoint, options, role);
+ const endpoint = `/api/resource/Lead/${encodeURIComponent(leadName)}`;
+ const options = {
+ method: "DELETE",
+ };
+ return await apiRequest(endpoint, options, role);
};
-// ------------------------------------------------------------------------------------------
\ No newline at end of file
+// ------------------------------------------------------------------------------------------
diff --git a/tests/e2e/utils/fileUtils.js b/tests/e2e/utils/fileUtils.js
index e2ab5c7950..92412e4a6b 100644
--- a/tests/e2e/utils/fileUtils.js
+++ b/tests/e2e/utils/fileUtils.js
@@ -1,6 +1,11 @@
import path from "path";
import fs from "fs";
import lockfile from "proper-lockfile";
+import { fileURLToPath } from "url";
+
+// Get __dirname equivalent in ES modules
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
// Import all data sources
@@ -127,11 +132,27 @@ export const writeDataToFile = async (filePath, data, maxRetries = 5) => {
} catch (lockErr) {
console.warn(`⚠️ Could not acquire lock for writing (attempt ${attempt}/${maxRetries}): ${absolutePath}`);
- // If file doesn't exist yet, create it without lock
+ // If file doesn't exist yet, create it atomically
if (lockErr.code === "ENOENT") {
- await fs.promises.writeFile(absolutePath, JSON.stringify(data, null, 2), "utf-8");
- console.log(`✅ Created new file: ${absolutePath}`);
- return;
+ try {
+ // 'wx' flag = write + exclusive (fails if file exists)
+ // This ensures only one worker creates the file
+ await fs.promises.writeFile(absolutePath, JSON.stringify(data, null, 2), {
+ encoding: "utf-8",
+ flag: "wx", // Creates only if doesn't exist
+ });
+ console.log(`✅ Created new file: ${absolutePath}`);
+ return;
+ } catch (writeErr) {
+ if (writeErr.code === "EEXIST") {
+ // Another worker just created it, retry with lock
+ console.warn(
+ `⚠️ File was created by another worker, retrying with lock (attempt ${attempt}/${maxRetries})`,
+ );
+ continue;
+ }
+ throw writeErr;
+ }
}
// For other lock errors, retry
diff --git a/tests/e2e/utils/stringUtils.js b/tests/e2e/utils/stringUtils.js
index 391f961667..539c0fb16f 100644
--- a/tests/e2e/utils/stringUtils.js
+++ b/tests/e2e/utils/stringUtils.js
@@ -2,6 +2,10 @@
* Returns a random String of provided length size
*/
export const getRandomString = (length) => {
+ if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) {
+ throw new Error("Length must be a positive integer");
+ }
+
const characters = "abcdefghijklmnopqrstuvwxyz";
let result = "";
for (let i = 0; i < length; i++) {
@@ -15,6 +19,9 @@ export const getRandomString = (length) => {
* Returns a random item from an array
*/
export const getRandomValue = (array) => {
+ if (!Array.isArray(array) || array.length === 0) {
+ throw new Error("getRandomValue: input must be a non-empty array");
+ }
const randomIndex = Math.floor(Math.random() * array.length);
return array[randomIndex];
};