UNPKG

tm-playwright-framework

Version:

Playwright Cucumber TS framework - The easiest way to learn

426 lines (425 loc) 19.7 kB
/** * Utility functions for API testing in a Playwright and Cucumber framework. * * Purpose: * This file provides helper functions to build API requests, write responses, * read previous responses, extract specific values from JSON responses, and manage runtime variables. * It is designed to streamline API testing by automating common tasks such as * dynamic value replacement, file handling, JSONPath queries, and file monitoring. * * Key Components: * - buildRequest: Constructs an API request by replacing dynamic placeholders * in a JSON template with actual values and writes the request to a file. * - replaceDynamicValues: Replaces placeholders in a JSON object with actual values. * - writeResponse: Writes the API response to a file for later reference. * - convertFileContentToJSONObject: Converts the content of a file into a JSON object. * - readPreviousResponse: Reads a previously saved API response from a file. * - extractResponseValueByJsonPath: Extracts a specific value from a JSON response using a JSONPath query. * - extractResponse: Retrieves the full response from a saved file. * - extractValueByJsonPath: Extracts a value from a JSON object using a JSONPath query. * - setRunTimeVariable: Sets a runtime variable for use in tests. * - getRunTimeVariable: Retrieves a runtime variable by key or all variables for a context. * - loadJson: Loads a JSON file from a relative path. * - waitForFileToDisappear: Waits for a file to disappear from a folder within a timeout. * - waitForFileToAppear: Waits for a file to appear in a folder within a timeout. * * Usage: * Import the required functions and use them to handle API requests, responses, and runtime variables * in your test scripts. Ensure that the necessary environment variables (e.g., * API_TEMPLATE_PATH, API_REPORT_PATH, REPORT_PATH) are set before using these utilities. * * Updates: * - Added utility functions for runtime variable management. * - Enhanced file monitoring with `waitForFileToAppear` and `waitForFileToDisappear`. * - Improved error handling and logging for better debugging. * * @author Sasitharan, Govindharam * @reviewer Sahoo, AshokKumar * @version 1.0 - 1st-JUNE-2025 */ import fs from "fs-extra"; import { JSONPath } from "jsonpath-plus"; import Logger from "tm-playwright-framework/dist/report/logger.js"; let localVariables = loadJson('app/test-data/localVariables.json'); import path from 'path'; import { fixture } from "tm-playwright-framework/dist/hooks/pageFixture.js"; const executedTests = {}; /** * Builds an API request by replacing dynamic placeholders in a JSON template with actual values. * Writes the constructed request to a file. * * @param FieldMappings - A mapping of placeholders to their actual values. * @param templatName - The name of the JSON template file. * @param iteration - The iteration number for the request. * @param templatePath - Optional path to the JSON template file. * @returns The constructed request as a JSON object. */ export function buildRequest(FieldMappings, templatName, iteration, templatePath) { if (!templatePath) templatePath = `${process.env.API_TEMPLATE_PATH}/${templatName}.json`; // Multiple Cucumber Report generation looks for all JSON files iteratively in the report folder and sub folders // Since request and response jsons are generated in the same folder report generation is failing // To avoid such failure request and responses jsons are being written in the parent folder of the report folder if (!process.env.API_REPORT_PATH || process.env.CUCUMBER_TEST === "true") process.env.API_REPORT_PATH = `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${process.env.CURRENT_TEST}`; fs.ensureDirSync(`${process.env.API_REPORT_PATH}`); let message = ""; let status = "Failed"; try { let jsonData = JSON.parse(fs.readFileSync(templatePath, "utf8")); const jsonPathResult = JSON.stringify(JSONPath({ json: jsonData, path: '$.Request' })[0], null, 2); fs.ensureDirSync(`${process.env.API_REPORT_PATH}`); let requestFileName = `${process.env.API_REPORT_PATH}/${templatName}_request_${iteration}.json`; let ModifiedfileData = JSON.parse(replaceDynamicValues(jsonPathResult, FieldMappings)); fs.writeFileSync(requestFileName, JSON.stringify(ModifiedfileData, null, 2), "utf8"); if (process.env.CUCUMBER_TEST === "true") { Logger.logStatus("Success", "Request body is built successfully. <a href='" + `./../../../${process.env.API_REPORT_PATH}/${templatName}_request_${iteration}.json` + "'>Click here to view the request body</a>"); } else { Logger.logStatus("Success", "Request body is built successfully. <a href='" + `./../../${process.env.API_REPORT_PATH}/${templatName}_request_${iteration}.json` + "'>Click here to view the request body</a>"); } return convertFileContentToJSONObject(`${process.env.API_REPORT_PATH}/${templatName}_request_${iteration}.json`); } catch (error) { status = 'Failed'; if (error instanceof Error) { message = `Builiding request body is failed. Error: ${error.message}`; Logger.logSoftAssertFailure(error); } else { message = "Builiding request body is failed. An unknown error occurred"; Logger.logSoftAssertFailure(error); } Logger.logStatus(status, message); return; } } /** * Replaces placeholders in a JSON object with actual values from a mapping. * * @param obj - The JSON object or string containing placeholders. * @param FieldMappings - A mapping of placeholders to their actual values. * @returns The JSON object or string with placeholders replaced. */ export function replaceDynamicValues(obj, FieldMappings) { if (typeof obj === 'string') { for (const [key, value] of Object.entries(FieldMappings)) { const baseRegex = new RegExp(`<${key}>`, 'g'); const numBolRegex = new RegExp(`"<(?:NUM|BOL)_${key}>"`, 'g'); // matches <NUM_key> or <BOL_key> const hasBaseMatch = baseRegex.test(obj); const hasNumBolMatch = numBolRegex.test(obj); if (hasBaseMatch) { obj = obj.replace(baseRegex, value); } else if (hasNumBolMatch) { obj = obj.replace(numBolRegex, value); } } } return obj; } /** * Writes an API response to a file for later reference. * * @param APIResponse - The API response to write. * @param templateName - The name of the template associated with the response. * @param iteration - The iteration number for the response. * @param extension - The file extension for the response file. */ export function writeResponse(APIResponse, templateName, iteration, extension) { let APIResponseBody; let message = ""; let status = "Failed"; try { if (templateName === undefined || templateName === null) { templateName = process.env.CURRENT_TEST; } APIResponseBody = JSON.stringify(APIResponse, null, 2); fs.writeFileSync(`${process.env.API_REPORT_PATH}/${templateName}_response_${iteration}${extension}`, APIResponseBody, "utf8", (err) => { if (err) { console.error("Error writing file:", err); } else { console.log("File has been written successfully."); } }); status = "Success"; if (process.env.CUCUMBER_TEST === "true") { message = "Response body is written successfully. <a href='" + `./../../../${process.env.API_REPORT_PATH}/${templateName}_response_${iteration}${extension}` + "'>Click here to view the response body</a>"; } else { message = "Response body is written successfully. <a href='" + `./../../${process.env.API_REPORT_PATH}/${templateName}_response_${iteration}${extension}` + "'>Click here to view the response body</a>"; } Logger.logStatus(status, message); } catch (error) { status = 'Failed'; if (error instanceof Error) { message = `Saving response as file is failed. Error: ${error.message}`; Logger.logSoftAssertFailure(error); } else { message = "Saving response as file is failed. An unknown error occurred"; Logger.logSoftAssertFailure(error); } Logger.logStatus(status, message); return; } } /** * Converts the content of a file into a JSON object. * * @param filePathToConvert - The path to the file to convert. * @returns The file content as a JSON object. */ export function convertFileContentToJSONObject(filePathToConvert) { let fileContent = fs.readFileSync(`${filePathToConvert}`, "utf8"); return JSON.parse(fileContent); } /** * Reads a previously saved API response from a file. * * @param ProofingSearchResponse - The name of the response file to read. * @returns The content of the response file as a JSON object. */ export function readPreviousResponse(ProofingSearchResponse) { let fileContent = fs.readFileSync(`${process.env.API_REPORT_PATH}/${ProofingSearchResponse}`, "utf8"); fs.close(); return JSON.parse(fileContent); } /** * Extracts a specific value from a JSON response using a JSONPath query. * * @param scenarioOrTestName - The name of the scenario or test. * @param templateName - The name of the template associated with the response. * @param jsonPath - The JSONPath query to extract the value. * @param iteration - The iteration number for the response (default: "1"). * @returns The extracted value or null if not found. */ export function extractResponseValueByJsonPath(scenarioOrTestName, templateName, jsonPath, iteration = "1") { try { // Multiple Cucumber Report generation looks for all JSON files iteratively in the report folder and sub folders // Since request and response jsons are generated in the same folder report generation is failing // To avoid such failure request and responses jsons are being written in the parent folder of the report folder let filePath; if (`${process.env.API_REPORT_PATH}` === `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${process.env.CURRENT_TEST}`) filePath = `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${scenarioOrTestName}`; else filePath = `${process.env.REPORT_PATH}/${scenarioOrTestName}`; const files = fs.readdirSync(`${filePath}`); const targetFile = files.find(file => file.endsWith(`${templateName}_response_${iteration}.json`)); if (!targetFile) { throw new Error(`No file ending with ${templateName}_response_${iteration}.json found.`); } let fullPath; if (`${process.env.API_REPORT_PATH}` === `${process.env.REPORT_PATH}/../${process.env.CURRENT_TEST}`) fullPath = `${process.env.REPORT_PATH}/../${scenarioOrTestName}/${targetFile}`; else fullPath = `${process.env.REPORT_PATH}/${scenarioOrTestName}/${targetFile}`; let PreviousResponseData = fs.readFileSync(`${fullPath}`, "utf8"); return extractValueByJsonPath(JSON.parse(PreviousResponseData), jsonPath); //return JSONPath({ json: JSON.parse(PreviousResponseData), path: jsonPath } // )[0]; } catch (error) { console.log(error); fixture.logger.error(error.message); fixture.logger.error(error.stack); Logger.logSoftAssertFailure(error); return null; } } /** * Retrieves the full response from a saved file. * * @param scenarioOrTestName - The name of the scenario or test. * @param templateName - The name of the template associated with the response. * @param iteration - The iteration number for the response (default: "1"). * @returns The full response as a string or null if not found. */ export function extractResponse(scenarioOrTestName, templateName, iteration = "1") { try { // Multiple Cucumber Report generation looks for all JSON files iteratively in the report folder and sub folders // Since request and response jsons are generated in the same folder report generation is failing // To avoid such failure request and responses jsons are being written in the parent folder of the report folder let filePath; if (`${process.env.API_REPORT_PATH}` === `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${process.env.CURRENT_TEST}`) filePath = `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${scenarioOrTestName}`; else filePath = `${process.env.REPORT_PATH}/${scenarioOrTestName}`; const files = fs.readdirSync(`${filePath}`); const targetFile = files.find(file => file.endsWith(`${templateName}_response_${iteration}.json`)); if (!targetFile) { throw new Error(`No file ending with ${templateName}_response_${iteration}.json found.`); } let fullPath; if (`${process.env.API_REPORT_PATH}` === `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${process.env.CURRENT_TEST}`) fullPath = `${process.env.REPORT_PATH}/../${process.env.ENV}/${process.env.CURRENT_FEATURE}/${scenarioOrTestName}/${targetFile}`; else fullPath = `${process.env.REPORT_PATH}/${scenarioOrTestName}/${targetFile}`; let PreviousResponseData = fs.readFileSync(`${fullPath}`, "utf8"); return PreviousResponseData; } catch (error) { console.log(error); fixture.logger.error(error.message); fixture.logger.error(error.stack); Logger.logSoftAssertFailure(error); return null; } } /** * Extracts a value from a JSON object using a JSONPath query. * * @param jsonValue - The JSON object to query. * @param jsonPath - The JSONPath query to extract the value. * @returns The extracted value or null if not found. */ export function extractValueByJsonPath(jsonValue, jsonPath) { try { let actualValues = JSONPath({ json: jsonValue, path: jsonPath }); let actualValue; if (actualValues.length === 1) { actualValue = actualValues[0]; } else if (actualValues.length > 1) { actualValue = actualValues; } else { // If the path includes a filter, handle manually const filterMatch = jsonPath.match(/\$\.(.+?)\[\?\@\.([^\]]+?)=='(.+?)'\]\.(.+)/); if (filterMatch) { const [, arrayPath, filterKey, filterValue, targetKey] = filterMatch; const arrayData = arrayPath.split('.').reduce((acc, key) => acc?.[key], jsonValue); if (Array.isArray(arrayData)) { const matched = arrayData.find((item) => item?.[filterKey] === filterValue); actualValue = matched?.[targetKey]; } } } return actualValue !== undefined ? actualValue : null; } catch (error) { console.log(error); fixture.logger.error(error.message); fixture.logger.error(error.stack); Logger.logSoftAssertFailure(error); return null; } } /** * Sets a runtime variable for use in tests. * * @param ctx - The test context. * @param key - The key for the runtime variable. * @param value - The value to set for the runtime variable. */ export function setRunTimeVariable(ctx, key, value) { const env = process.env.ENV || 'dev'; if (!localVariables[env]) { localVariables[env] = {}; } if (!localVariables[env][ctx]) { localVariables[env][ctx] = {}; } localVariables[env][ctx][key] = value; ctx.runData[key] = value; Logger.logStatus("Success", `<p style='color:rgb(151, 147, 187);'> >> Variable has been added : ${key} = <b>${value}</b></p>`); } /** * Retrieves a runtime variable by key or all variables for a context. * * @param ctx - The test context. * @param key - Optional key to retrieve a specific variable. * @returns The value of the runtime variable or all variables for the context. */ export function getRunTimeVariable(ctx, key) { try { const env = process.env.ENV || 'dev'; const ctxData = localVariables?.[env]?.[ctx]; if (!ctxData) return null; return key ? ctxData[key] : ctxData; } catch (error) { console.error(error); fixture.logger.error(error.message); fixture.logger.error(error.stack); Logger.logSoftAssertFailure(error); return null; } } /** * Loads a JSON file from a relative path. * * @param relativePath - The relative path to the JSON file. * @returns The content of the JSON file as a JSON object. */ export async function loadJson(relativePath) { const absolutePath = path.resolve(relativePath); const data = await fs.readFile(absolutePath, 'utf-8'); return await JSON.parse(data); } /** * Waits for a file to disappear from a folder or until the timeout is reached. * * @param folderPath - The folder to watch. * @param fileName - The name of the file to watch for. * @param timeoutMs - Timeout in milliseconds. * @param checkIntervalMs - How often to check (default: 1000ms). * @returns Promise that resolves if the file disappears or rejects if timeout is reached. */ export async function waitForFileToDisappear(folderPath, fileName, timeoutMs, checkIntervalMs = 1000) { const filePath = path.join(folderPath, fileName); return new Promise((resolve, reject) => { const start = Date.now(); const interval = setInterval(async () => { try { await fs.access(filePath); const elapsed = Date.now() - start; if (elapsed >= timeoutMs) { clearInterval(interval); reject(new Error(`Timeout: File "${fileName}" still exists after ${timeoutMs}ms.`)); } } catch { // File not found (has disappeared) clearInterval(interval); resolve(); } }, checkIntervalMs); }); } /** * Waits for a file to appear in a folder or until timeout is reached. * * @param folderPath - The folder to monitor. * @param fileName - The target file to detect. * @param timeoutMs - How long to wait before giving up (in ms). * @param checkIntervalMs - How often to poll the folder (default: 1000ms). * @returns Promise that resolves when the file appears, or rejects on timeout. */ export async function waitForFileToAppear(folderPath, fileName, timeoutMs, checkIntervalMs = 1000) { const filePath = path.join(folderPath, fileName); const start = Date.now(); return new Promise((resolve, reject) => { const interval = setInterval(async () => { try { await fs.access(filePath); clearInterval(interval); resolve(); } catch { if (Date.now() - start >= timeoutMs) { clearInterval(interval); reject(new Error(`Timeout: File "${fileName}" did not appear in ${timeoutMs}ms.`)); } // else: file not found, continue polling } }, checkIntervalMs); }); }