From 2611781bbb255bec1e9162a92d97586f5060824e Mon Sep 17 00:00:00 2001 From: Manoj Maharrshi <110522049+maharrshi@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:49:15 +0530 Subject: [PATCH 01/16] Potential fix for code scanning alert no. 5: Code injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/e2e-playwright-test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index a1e68ed7e6..e5c9f9950e 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -247,13 +247,15 @@ 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 + echo "message=

For the latest pull request merge titled $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 From 71460072f2f632a3ae3b593f92d4d64ccae7e875 Mon Sep 17 00:00:00 2001 From: maharrshi Date: Tue, 4 Nov 2025 19:01:13 +0530 Subject: [PATCH 02/16] fix : lint issues --- tests/e2e/.env.example | 2 +- tests/e2e/README.md | 1 - tests/e2e/data/admin/lead.js | 2 +- tests/e2e/globals/globalSetup.js | 1 - tests/e2e/globals/globalTeardown.js | 1 - tests/e2e/helpers/frappeRequests.js | 5 +- tests/e2e/helpers/leadTabHelper.js | 113 +++++++------- tests/e2e/pageObjects/leadsPage.js | 214 +++++++++++++------------- tests/e2e/playwright.config.js | 6 +- tests/e2e/playwright.fixture.cjs | 6 +- tests/e2e/scripts/list-tests.js | 2 +- tests/e2e/specs/admin/lead.spec.js | 49 +++--- tests/e2e/utils/api/frappeRequests.js | 5 +- tests/e2e/utils/api/leadRequests.js | 87 ++++++----- 14 files changed, 243 insertions(+), 251 deletions(-) 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..7b64ab8afe 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,4 +1,3 @@ # Next-CRM Playwright E2E Test Automation Framework ## Table of Contents - 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..b6faf5c0fe 100644 --- a/tests/e2e/globals/globalSetup.js +++ b/tests/e2e/globals/globalSetup.js @@ -79,7 +79,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..e074284105 100644 --- a/tests/e2e/globals/globalTeardown.js +++ b/tests/e2e/globals/globalTeardown.js @@ -25,7 +25,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 index f73db2346a..6eeb5100df 100644 --- a/tests/e2e/helpers/frappeRequests.js +++ b/tests/e2e/helpers/frappeRequests.js @@ -55,7 +55,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 +93,7 @@ export const filterApi = async (docType, filters, role = "admin") => { method: "POST", form: formPayload, }, - role + role, ); }; // ------------------------------------------------------------------------------------------ - diff --git a/tests/e2e/helpers/leadTabHelper.js b/tests/e2e/helpers/leadTabHelper.js index 8ad6b813b1..064a0b032e 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[]} testCaseIDs 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; - } + 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; + } - 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[]} testCaseIDs 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]]); + 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; + } - // 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/pageObjects/leadsPage.js b/tests/e2e/pageObjects/leadsPage.js index 76df763169..7b09dfeb82 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..4a60050efb 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -1,11 +1,10 @@ // @ts-check const { defineConfig, devices } = require("@playwright/test"); import path from "path"; -import dotenv from 'dotenv'; - +import dotenv from "dotenv"; // Load environment variables from .env in this folder -dotenv.config({ path: './.env' }); +dotenv.config({ path: "./.env" }); /** * @see https://playwright.dev/docs/test-configuration @@ -79,6 +78,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.cjs index a544720bed..9d74ecfdf6 100644 --- a/tests/e2e/playwright.fixture.cjs +++ b/tests/e2e/playwright.fixture.cjs @@ -10,7 +10,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 +38,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) @@ -48,7 +48,7 @@ const test = base.extend({ } catch { console.error("❌ JSON directory missing - was global setup run?"); throw new Error( - "JSON directory not found. Ensure global setup has run." + "JSON directory not found. Ensure global setup has run.", ); } diff --git a/tests/e2e/scripts/list-tests.js b/tests/e2e/scripts/list-tests.js index 3a56abb432..71c1003042 100644 --- a/tests/e2e/scripts/list-tests.js +++ b/tests/e2e/scripts/list-tests.js @@ -91,7 +91,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..b66a066a44 100644 --- a/tests/e2e/specs/admin/lead.spec.js +++ b/tests/e2e/specs/admin/lead.spec.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..6eeb5100df 100644 --- a/tests/e2e/utils/api/frappeRequests.js +++ b/tests/e2e/utils/api/frappeRequests.js @@ -55,7 +55,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 +93,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..7cfde73643 100644 --- a/tests/e2e/utils/api/leadRequests.js +++ b/tests/e2e/utils/api/leadRequests.js @@ -11,11 +11,11 @@ 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 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; }; // ------------------------------------------------------------------------------------------ @@ -23,42 +23,41 @@ const loadAuthState = (role) => { * Helper function to load 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 +65,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 +78,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 +// ------------------------------------------------------------------------------------------ From c957cb9e04a05cca8a63bed27b02e08d8caffbb9 Mon Sep 17 00:00:00 2001 From: maharrshi Date: Tue, 4 Nov 2025 19:13:24 +0530 Subject: [PATCH 03/16] fix : security issue for pipeline --- .github/workflows/e2e-playwright-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index e5c9f9950e..623a8363a5 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -252,7 +252,9 @@ jobs: 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 $PR_TITLE, the E2E automation tests have been executed on the QE environment. Below is the summary:

" >> $GITHUB_ENV + # 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 merge 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 From 72ebcc09af4fcafb5a8a52f49fed290edc93d70b Mon Sep 17 00:00:00 2001 From: maharrshi Date: Tue, 4 Nov 2025 19:32:39 +0530 Subject: [PATCH 04/16] fix : playwright workflow --- .github/workflows/e2e-playwright-test.yml | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index 623a8363a5..5cf038c3f8 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -16,7 +16,9 @@ jobs: test: if: | - (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') || success() + github.event_name == 'workflow_dispatch' || + github.event_name == 'schedule' || + (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') runs-on: self-hosted container: image: ubuntu:24.04 @@ -43,13 +45,13 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version-file: "frontend/.nvmrc" + node-version: "18" - name: Cache npm modules uses: actions/cache@v4 with: - path: /github/home/.npm - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- @@ -68,9 +70,9 @@ jobs: - name: Sanitize branch name shell: bash 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="${{ github.event.pull_request.head.ref }}" else # workflow_dispatch or schedule → use whatever ref we were on RAW_BRANCH="${GITHUB_REF#refs/heads/}" @@ -118,8 +120,8 @@ jobs: # Install dependencies - name: Install dependencies run: | - npm ci cd tests/e2e/ + npm ci npx playwright install --with-deps - name: Setup Java @@ -251,10 +253,10 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} PR_URL: ${{ github.event.pull_request.html_url }} run: | - if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then + 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 merge titled $SANITIZED_PR_TITLE, the E2E automation tests have been executed on the QE environment. Below is the summary:

" >> $GITHUB_ENV + 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 @@ -266,7 +268,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: | From 5ac9e9970fe3077f623a2eb989a063577a4261ce Mon Sep 17 00:00:00 2001 From: maharrshi Date: Tue, 4 Nov 2025 19:56:36 +0530 Subject: [PATCH 05/16] fix : updated read me file --- tests/e2e/README.md | 336 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 7b64ab8afe..48d5112a23 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,3 +1,339 @@ # 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.cjs # 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 From 2b11721101990c1692e2f3634e57002db56da704 Mon Sep 17 00:00:00 2001 From: maharrshi Date: Tue, 4 Nov 2025 20:01:50 +0530 Subject: [PATCH 06/16] fix : lint issue --- tests/e2e/README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 48d5112a23..c78f69c81b 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -100,6 +100,7 @@ npm ci ``` This will install: + - Playwright Test - Playwright browsers - Allure Playwright reporter @@ -225,6 +226,7 @@ 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 @@ -259,6 +261,7 @@ The framework includes a complete CI/CD pipeline (`.github/workflows/e2e-playwri ### Running Tests on CI Tests automatically run when: + - A PR is approved - Daily at the scheduled time - Manually triggered from GitHub Actions @@ -276,33 +279,36 @@ Tests automatically run when: ### 2. Stable Locators Prefer stable locators: + ```javascript // Good - Data attributes -page.locator('[data-testid="create-button"]') +page.locator('[data-testid="create-button"]'); // Good - Role-based -page.getByRole('button', { name: 'Create' }) +page.getByRole("button", { name: "Create" }); // Avoid - CSS classes (can change) -page.locator('.btn-primary') +page.locator(".btn-primary"); ``` ### 3. Explicit Waits Always use explicit waits: + ```javascript // Wait for element to be visible -await expect(page.locator('#element')).toBeVisible(); +await expect(page.locator("#element")).toBeVisible(); // Wait for specific state -await page.waitForLoadState('networkidle'); +await page.waitForLoadState("networkidle"); ``` ### 4. Error Handling Add meaningful error messages: + ```javascript -expect(result, 'Lead should be created successfully').toBe(true); +expect(result, "Lead should be created successfully").toBe(true); ``` ### 5. Code Reusability @@ -336,4 +342,5 @@ expect(result, 'Lead should be created successfully').toBe(true); ## Support For questions or issues: + - Contact the QA team From b53a0069b253bb857e4b8fa121c941ee10e75481 Mon Sep 17 00:00:00 2001 From: Manoj Maharrshi <110522049+maharrshi@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:53:52 +0530 Subject: [PATCH 07/16] Potential fix for code scanning alert no. 9: Code injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/e2e-playwright-test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index 5cf038c3f8..87fcc2aee4 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -69,10 +69,12 @@ jobs: - name: Sanitize branch name shell: bash + env: + UNTRUSTED_PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | if [ "${{ github.event_name }}" = "pull_request_review" ]; then # PR Review → use the source branch name - RAW_BRANCH="${{ github.event.pull_request.head.ref }}" + RAW_BRANCH="$UNTRUSTED_PR_HEAD_REF" else # workflow_dispatch or schedule → use whatever ref we were on RAW_BRANCH="${GITHUB_REF#refs/heads/}" @@ -81,7 +83,6 @@ 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 }}" From d299c81af53b2e096975c743b230b40608248127 Mon Sep 17 00:00:00 2001 From: Manoj Maharrshi <110522049+maharrshi@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:55:46 +0530 Subject: [PATCH 08/16] Potential fix for code scanning alert no. 10: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/e2e-playwright-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index 87fcc2aee4..a7f914604c 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: From 40a6056b9e8b686749baaf025b566f07721bf920 Mon Sep 17 00:00:00 2001 From: maharrshi Date: Fri, 7 Nov 2025 19:45:37 +0530 Subject: [PATCH 09/16] fix : review suggestions --- tests/e2e/globals/globalSetup.js | 1 - tests/e2e/helpers/leadTabHelper.js | 114 ++++++++++++++--------------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/tests/e2e/globals/globalSetup.js b/tests/e2e/globals/globalSetup.js index b6faf5c0fe..59839c0d92 100644 --- a/tests/e2e/globals/globalSetup.js +++ b/tests/e2e/globals/globalSetup.js @@ -3,7 +3,6 @@ import { createJSONFile, populateJsonStubs } from "../utils/fileUtils"; import path from "path"; import fs from "fs"; import { execSync } from "child_process"; -import { create } from "domain"; import { createLeadForTestCases } from "../helpers/leadTabHelper"; const globalSetup = async () => { diff --git a/tests/e2e/helpers/leadTabHelper.js b/tests/e2e/helpers/leadTabHelper.js index 064a0b032e..47305feb11 100644 --- a/tests/e2e/helpers/leadTabHelper.js +++ b/tests/e2e/helpers/leadTabHelper.js @@ -7,77 +7,77 @@ import { filterApi } from "../utils/api/frappeRequests.js"; /** * Creates Lead(s) for provided testCaseID(s). - * @param {string[]} testCaseIDs IDs to process + * @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[]} 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; - } +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; + } - try { - const leadRes = await filterApi("Lead", [["Lead", "company_name", "=", entry.infoPayloadCreateLead.company_name]]); + try { + const leadRes = await filterApi("Lead", [["Lead", "company_name", "=", entry.infoPayloadCreateLead.company_name]]); - // 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]); + // 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]); - // 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; - } - }); + // 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; + } + }); - // 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}'`); + // 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}`, + ); } - } catch (error) { - console.error( - `❌ Failed to delete Lead for '${tcId}' with company_name '${entry.infoPayloadCreateLead.company_name}': ${error.message}`, - ); - } }; From f36af33aeac1a73539d19f575b8a36e9f4bbc1e5 Mon Sep 17 00:00:00 2001 From: maharrshi Date: Fri, 7 Nov 2025 19:57:04 +0530 Subject: [PATCH 10/16] fix : lint issues --- tests/e2e/helpers/leadTabHelper.js | 106 ++++++++++++++--------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/tests/e2e/helpers/leadTabHelper.js b/tests/e2e/helpers/leadTabHelper.js index 47305feb11..bfe6810b76 100644 --- a/tests/e2e/helpers/leadTabHelper.js +++ b/tests/e2e/helpers/leadTabHelper.js @@ -11,22 +11,22 @@ import { filterApi } from "../utils/api/frappeRequests.js"; * @param {string} jsonDir Directory where JSON stubs are located */ 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; - } + 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}`); + } }; // ------------------------------------------------------------------------------------------ @@ -37,47 +37,47 @@ export const createLeadForTestCases = async (testCaseID, jsonDir) => { * @param {string} jsonDir Directory where JSON stubs are located */ 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; - } + 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; + } - try { - const leadRes = await filterApi("Lead", [["Lead", "company_name", "=", entry.infoPayloadCreateLead.company_name]]); + try { + const leadRes = await filterApi("Lead", [["Lead", "company_name", "=", entry.infoPayloadCreateLead.company_name]]); - // 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]); + // 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]); - // 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; - } - }); - - // 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}`, + ); + } }; From cf49871d0930e6a6462311f21268d88857bab6df Mon Sep 17 00:00:00 2001 From: maharrshi Date: Wed, 12 Nov 2025 13:04:40 +0530 Subject: [PATCH 11/16] fix : copilot suggestions refactor : code to ES modules --- .github/workflows/e2e-playwright-test.yml | 2 +- tests/e2e/globals/globalSetup.js | 4 + tests/e2e/globals/globalTeardown.js | 4 + tests/e2e/helpers/frappeRequests.js | 99 ------------------- tests/e2e/helpers/storageStateHelper.js | 4 + tests/e2e/package.json | 1 + tests/e2e/playwright.config.js | 10 +- ...ight.fixture.cjs => playwright.fixture.js} | 14 ++- tests/e2e/scripts/list-tests.js | 13 ++- tests/e2e/specs/admin/lead.spec.js | 2 +- tests/e2e/utils/api/frappeRequests.js | 4 + tests/e2e/utils/api/leadRequests.js | 5 +- tests/e2e/utils/fileUtils.js | 27 ++++- tests/e2e/utils/stringUtils.js | 7 ++ 14 files changed, 78 insertions(+), 118 deletions(-) delete mode 100644 tests/e2e/helpers/frappeRequests.js rename tests/e2e/{playwright.fixture.cjs => playwright.fixture.js} (84%) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index a7f914604c..e382d1374e 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -24,7 +24,7 @@ jobs: runs-on: self-hosted container: image: ubuntu:24.04 - environment: Stage # Retrive secrets from 'Stage' environment + environment: Stage # Retrieve secrets from 'Stage' environment env: BASE_URL: ${{ secrets.BASE_URL }} diff --git a/tests/e2e/globals/globalSetup.js b/tests/e2e/globals/globalSetup.js index 59839c0d92..e486ccdf21 100644 --- a/tests/e2e/globals/globalSetup.js +++ b/tests/e2e/globals/globalSetup.js @@ -3,8 +3,12 @@ import { createJSONFile, populateJsonStubs } from "../utils/fileUtils"; import path from "path"; import fs from "fs"; import { execSync } from "child_process"; +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..."); diff --git a/tests/e2e/globals/globalTeardown.js b/tests/e2e/globals/globalTeardown.js index e074284105..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. */ diff --git a/tests/e2e/helpers/frappeRequests.js b/tests/e2e/helpers/frappeRequests.js deleted file mode 100644 index 6eeb5100df..0000000000 --- a/tests/e2e/helpers/frappeRequests.js +++ /dev/null @@ -1,99 +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/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..1ccc0288c8 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,7 @@ { "name": "e2e", "version": "1.0.0", + "type": "module", "main": "index.js", "scripts": { "e2e:tests": "npx playwright test --config=tests/e2e/playwright.config.js", diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 4a60050efb..4128d3c2e5 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -1,7 +1,11 @@ // @ts-check -const { defineConfig, devices } = require("@playwright/test"); +import { defineConfig, devices } from "@playwright/test"; import path from "path"; 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" }); @@ -9,7 +13,7 @@ dotenv.config({ path: "./.env" }); /** * @see https://playwright.dev/docs/test-configuration */ -module.exports = defineConfig({ +export default defineConfig({ /* Global setup file */ globalSetup: "./globals/globalSetup.js", @@ -35,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: [ diff --git a/tests/e2e/playwright.fixture.cjs b/tests/e2e/playwright.fixture.js similarity index 84% rename from tests/e2e/playwright.fixture.cjs rename to tests/e2e/playwright.fixture.js index 9d74ecfdf6..cba2d6e75f 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"); @@ -63,4 +67,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 71c1003042..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"); diff --git a/tests/e2e/specs/admin/lead.spec.js b/tests/e2e/specs/admin/lead.spec.js index b66a066a44..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"; diff --git a/tests/e2e/utils/api/frappeRequests.js b/tests/e2e/utils/api/frappeRequests.js index 6eeb5100df..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; // ------------------------------------------------------------------------------------------ diff --git a/tests/e2e/utils/api/leadRequests.js b/tests/e2e/utils/api/leadRequests.js index 7cfde73643..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,6 +12,8 @@ const baseURL = config.use?.baseURL; * Helper function to ensure storage state is loaded for respective roles. */ const loadAuthState = (role) => { + 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}`); @@ -20,7 +23,7 @@ const loadAuthState = (role) => { // ------------------------------------------------------------------------------------------ /** - * 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); diff --git a/tests/e2e/utils/fileUtils.js b/tests/e2e/utils/fileUtils.js index e2ab5c7950..b31ea9e490 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,25 @@ 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]; }; From 6943c7f077d5784266cd535957283ba9e2460100 Mon Sep 17 00:00:00 2001 From: maharrshi Date: Wed, 12 Nov 2025 13:11:09 +0530 Subject: [PATCH 12/16] fix : lint issues --- tests/e2e/playwright.fixture.js | 4 +--- tests/e2e/utils/fileUtils.js | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/playwright.fixture.js b/tests/e2e/playwright.fixture.js index cba2d6e75f..0f24d1063e 100644 --- a/tests/e2e/playwright.fixture.js +++ b/tests/e2e/playwright.fixture.js @@ -51,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); diff --git a/tests/e2e/utils/fileUtils.js b/tests/e2e/utils/fileUtils.js index b31ea9e490..92412e4a6b 100644 --- a/tests/e2e/utils/fileUtils.js +++ b/tests/e2e/utils/fileUtils.js @@ -146,7 +146,9 @@ export const writeDataToFile = async (filePath, data, maxRetries = 5) => { } 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})`); + console.warn( + `⚠️ File was created by another worker, retrying with lock (attempt ${attempt}/${maxRetries})`, + ); continue; } throw writeErr; From fd7210ac836ff07a2a82f29db8d4d031cd0a41c2 Mon Sep 17 00:00:00 2001 From: Manoj Maharrshi <110522049+maharrshi@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:31:24 +0530 Subject: [PATCH 13/16] Update tests/e2e/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/e2e/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index c78f69c81b..99f1242882 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -152,7 +152,7 @@ tests/e2e/ ├── README.md # This file ├── package.json # Node.js dependencies and scripts ├── playwright.config.js # Playwright test configuration -├── playwright.fixture.cjs # Custom fixtures (auth, test data) +├── playwright.fixture.js # Custom fixtures (auth, test data) │ ├── auth/ # Authentication state files │ ├── admin-API.json # API-only auth state From 6cbb50618c8c9c07eaf95211a7a3c8d58cbc1a8b Mon Sep 17 00:00:00 2001 From: maharrshi Date: Wed, 12 Nov 2025 13:32:29 +0530 Subject: [PATCH 14/16] fix indentation --- tests/e2e/pageObjects/leadsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/pageObjects/leadsPage.js b/tests/e2e/pageObjects/leadsPage.js index 7b09dfeb82..ad6e7faaea 100644 --- a/tests/e2e/pageObjects/leadsPage.js +++ b/tests/e2e/pageObjects/leadsPage.js @@ -98,7 +98,7 @@ export class LeadsPage { // ------------------------------------------------------------------------------------------ /** * Verify the lead details in side bar - * * @param {Object} tcData Test case data containing lead details + * @param {Object} tcData Test case data containing lead details * @returns {boolean} true if all verifications are passed */ async verifyLeadDetailsInSideBar(tcData) { From db5d7a518eb64e0d1f6f8e3f7fcfbad7e82f8967 Mon Sep 17 00:00:00 2001 From: Manoj Maharrshi Date: Wed, 3 Dec 2025 18:29:20 +0530 Subject: [PATCH 15/16] fix : update working directory --- .github/workflows/e2e-playwright-test.yml | 69 +++++++++-------------- tests/e2e/package.json | 2 +- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index e382d1374e..1ca8af9fa5 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -18,13 +18,14 @@ jobs: test: if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + github.event_name == 'schedule' || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') - runs-on: self-hosted - container: - image: ubuntu:24.04 + runs-on: ubuntu-latest environment: Stage # Retrieve secrets from 'Stage' environment + defaults: + run: + working-directory: tests/e2e env: BASE_URL: ${{ secrets.BASE_URL }} @@ -49,11 +50,19 @@ jobs: with: 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: ~/.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- @@ -64,11 +73,6 @@ 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: @@ -95,6 +99,7 @@ jobs: 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/ @@ -107,6 +112,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..." @@ -123,20 +129,11 @@ jobs: # Install dependencies - name: Install dependencies run: | - cd tests/e2e/ npm ci 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() @@ -146,25 +143,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/ \ @@ -174,7 +165,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" @@ -222,7 +213,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)" @@ -232,19 +223,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 @@ -409,6 +399,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/package.json b/tests/e2e/package.json index 1ccc0288c8..3e0ed12642 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -4,7 +4,7 @@ "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", From eaa40511a5748f314d2676484bd0420141288738 Mon Sep 17 00:00:00 2001 From: Manoj Maharrshi Date: Wed, 3 Dec 2025 18:48:57 +0530 Subject: [PATCH 16/16] print cwd --- .github/workflows/e2e-playwright-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-playwright-test.yml b/.github/workflows/e2e-playwright-test.yml index 1ca8af9fa5..01b8e4bfeb 100644 --- a/.github/workflows/e2e-playwright-test.yml +++ b/.github/workflows/e2e-playwright-test.yml @@ -94,6 +94,7 @@ jobs: 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