tm-playwright-framework
Version:
Playwright Cucumber TS framework - The easiest way to learn
426 lines (425 loc) • 19.7 kB
JavaScript
/**
* 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);
});
}