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