tm-playwright-framework
Version:
Playwright Cucumber TS framework - The easiest way to learn
287 lines (286 loc) • 13 kB
JavaScript
/**
* APIASSERTIONS.TS
*
* This TypeScript file contains methods to perform various verifications for API(Assertions).
*
* @author Sasitharan, Govindharam
* @reviewer Sahoo, AshokKumar
* @version 1.0 - 1st-JUNE-2025
*
* @methods
* - `compareWithSharedJsonByJsonPath`: Accepts API Response and template name and compare Shared Json against actual response using JsonPath.
* - `compareWithJsonByJsonPath`: Accepts API Response and template name and compare test case Json against actual response using JsonPath.
* - `compareJSON`: Accepts API Response and template name and compare test case Json against actual response.
* - `compareSharedJSON`: Accepts API Response and template name and compare Shared Json against actual response.
* - `getComparisonJson`: Internal method to get comparison JSON from template.
* - `compareByJsonPath`: Internal method to compare two JSONs using Json Path.
* - `jsonComparison`: Internal method to compare two JSONs.
**/
import { expect } from "@playwright/test";
import Logger from "tm-playwright-framework/dist/report/logger.js";
import { JSONPath } from "jsonpath-plus";
import fs from "fs-extra";
import { replaceDynamicValues } from "tm-playwright-framework/dist/api/api_utility.js";
/**
* The ApiAssertion class provides methods to perform API response validation
* using JSONPath and JSON comparison techniques. It is designed to work
* with Playwright's APIResponse objects and supports shared JSON templates
* for validation.
*/
export default class ApiAssertion {
constructor(page) { this.page = page; }
/**
* Compares the API response with a shared JSON template using JSONPath.
* @param response - The API response object.
* @param templateName - The name of the shared JSON template.
*/
async compareWithSharedJsonByJsonPath(response, templateName) {
const jsonPathList = await this.getComparisonJson(templateName, "SharedResponse");
await this.compareByJsonPath(jsonPathList, await response.json());
return;
}
/**
* Compares the API response with a test case JSON template using JSONPath.
* @param response - The API response object.
* @param templateName - The name of the test case JSON template.
*/
// Accepts element locator as string or Element and asserts the element has the given text in it
async compareWithJsonByJsonPath(response, templateName) {
const currentTest = process.env.CURRENT_TEST;
const jsonPathList = await this.getComparisonJson(templateName, currentTest);
const respJson = await response.json();
await this.compareByJsonPath(jsonPathList, respJson);
}
/**
* Compares the API response with a JSON template.
* @param response - The API response object or JSON string.
* @param templateName - The name of the JSON template.
* @param testCaseName - Optional test case name for the comparison.
* @param replaceValues - Optional values to replace in the template.
* @returns A list of mismatches, if any.
*/
async compareJSON(response, templateName, testCaseName, replaceValues) {
let message = "";
let status = "";
let currentTest = "";
if (process.env.CUCUMBER_TEST === "true")
currentTest = testCaseName;
else
currentTest = process.env.CURRENT_TEST;
let expected = await this.getComparisonJson(templateName, currentTest);
if (replaceValues) {
expected = JSON.parse(replaceDynamicValues(JSON.stringify(expected), replaceValues));
}
let actual;
if (response && typeof response.json === 'function') {
actual = await response.json();
}
else if (typeof response === 'string') {
actual = JSON.parse(response);
}
else {
actual = response; // fallback: response is already an object
}
let mismatches = await this.jsonComparison(expected, actual);
if (mismatches.length === 0) {
status = "Success";
message = `<font color=green><b>PASS: All expected JSON entries found in the actual response"</b></font>`;
}
else {
status = "Failed";
message = `<font color=red><b>FAIL: All expected JSON entries not found in the actual response"`;
for (const { path, expected, actual } of mismatches) {
message += `- Path: ${path}\n Expected: ${expected}\n Actual: ${actual}`;
}
message += "</b></font>";
}
Logger.logStatus(status, message);
await expect(mismatches.length).toEqual(0);
return mismatches;
}
/**
* Compares the API response with a shared JSON template.
* @param response - The API response object.
* @param replaceValues - Optional values to replace in the template.
* @param templateName - The name of the shared JSON template.
* @returns A list of mismatches, if any.
*/
async compareSharedJSON(response, templateName, replaceValues) {
const currentTest = process.env.CURRENT_TEST;
let expected = await this.getComparisonJson(templateName, "SharedResponse");
const actual = await response.json();
if (replaceValues) {
expected = JSON.parse(replaceDynamicValues(JSON.stringify(expected), replaceValues));
}
let mismatches = await this.jsonComparison(expected, actual);
if (mismatches.length === 0) {
console.log("✅ All matched!");
}
else {
console.error("❌ Mismatches found:");
for (const { path, expected, actual } of mismatches) {
console.error(`- Path: ${path}\n Expected: ${expected}\n Actual: ${actual}`);
}
}
return mismatches;
}
/**
* Retrieves the comparison JSON from a template.
* @param templateName - The name of the JSON template.
* @param testCaseName - The test case name to retrieve the JSON for.
* @returns The JSONPath list for the specified test case.
*/
async getComparisonJson(templateName, testCaseName) {
const templatePath = `${process.env.API_TEMPLATE_PATH}/${templateName}.json`;
let jsonData = JSON.parse(fs.readFileSync(templatePath, "utf8"));
const jsonPathList = JSONPath({ json: jsonData, path: `$.${testCaseName}` })[0];
return jsonPathList;
}
/**
* Compares two JSON objects using JSONPath.
* @param jsonPathList - The JSONPath list to compare.
* @param respJson - The actual JSON response.
* @returns A boolean indicating the success of the comparison.
*/
async compareByJsonPath(jsonPathList, respJson) {
let status = 'Success';
let message = '';
let actualText = "";
let text = "";
let overAllResult = true;
try {
for (const [jsonPath, expectedRaw] of Object.entries(jsonPathList)) {
const expectedValue = expectedRaw.toString().replace('AssertEqual##', '');
const actualValues = JSONPath({ json: respJson, path: jsonPath });
// If JSONPath returns multiple, you might want to iterate — here we assert the first one
let actualValue;
if (typeof actualValues === "string")
actualValue = actualValues;
else
actualValue = actualValues[0];
// If we get a result, return it
if (!actualValue || actualValue.length === 0) {
// If the path includes a filter, handle manually
const filterMatch = jsonPath.match(/\$\.(.+?)\[\?\@\.([^\]]+?)=='(.+?)'\]\.(.+)/);
if (filterMatch) {
const [, arrayPath, filterKey, filterValue, targetKey] = filterMatch;
// Walk down to the array
const arrayData = arrayPath.split('.').reduce((acc, key) => acc?.[key], respJson);
if (Array.isArray(arrayData)) {
const matched = arrayData.find((item) => item?.[filterKey] === filterValue);
actualValue = matched?.[targetKey];
}
}
}
// Optional: Type coercion to match boolean, number, or string
const parsedExpected = parseValue(expectedValue);
if (actualValue !== parsedExpected) {
message = `<font color=red><b>FAIL: Mismatch at ${jsonPath}: expected "${parsedExpected}", got "${actualValue}"</b></font>`;
overAllResult = overAllResult && false;
}
else {
message = `<font color=green><b>PASS: Match at ${jsonPath}: "${actualValue}"</b></font>`;
overAllResult = overAllResult && true;
}
Logger.logStatus(status, message);
}
}
catch (error) {
status = 'Failed';
if (error instanceof Error) {
message = `${error.message}. For locator:`;
}
else {
message = "An unknown error occurred";
}
}
finally {
const result = overAllResult === true;
if (result) {
message = `<font color=green><b>PASS: Json Path Comparison has been passed successfully"</b></font>`;
}
else {
status = 'Failed';
message = `<font color=red><b>FAIL: Json Path Comparison has been failed"</b></font>`;
}
Logger.logStatus(status, message);
return await expect(overAllResult).toEqual(true);
}
}
/**
* Performs a deep comparison between two JSON objects.
* @param expected - The expected JSON object.
* @param actual - The actual JSON object.
* @param path - The current JSON path (used for recursive calls).
* @returns A list of mismatches, if any.
*/
async jsonComparison(expected, actual, path = '') {
let mismatches = [];
if (typeof expected !== 'object' || expected === null) {
if (expected !== '<ignore>') {
if (typeof actual === 'object' && actual !== null) {
// Type mismatch: expected primitive, got object
mismatches.push({ path, expected, actual });
}
else {
const isRegex = typeof expected === 'string' && expected.startsWith('/') && expected.endsWith('/');
if (isRegex) {
const regexBody = expected.slice(1, -1);
const regex = new RegExp(regexBody);
if (!regex.test(String(actual))) {
mismatches.push({ path, expected, actual });
}
else {
Logger.logStatus("Success", `JSON match at ${path}: expected "${expected}", got "${actual}"`);
}
}
else if (expected !== actual) {
mismatches.push({ path, expected, actual });
}
else {
Logger.logStatus("Success", `JSON match at ${path}: expected "${expected}", got "${actual}"`);
}
}
}
return mismatches;
}
// Handle arrays
if (Array.isArray(expected)) {
if (!Array.isArray(actual)) {
mismatches.push({ path, expected, actual });
return mismatches;
}
for (let i = 0; i < expected.length; i++) {
const subPath = `${path}[${i}]`;
mismatches = mismatches.concat(await this.jsonComparison(expected[i], actual[i], subPath));
}
return mismatches;
}
// Handle objects
//if (actual === null || typeof actual !== 'object') {
// mismatches.push({ path, expected, actual });
// return mismatches;
//}
for (const key of Object.keys(expected)) {
const subPath = path ? `${path}.${key}` : key;
if (expected[key] === '<ignore>') {
continue;
}
if (!(key in actual)) {
mismatches.push({ path: subPath, expected: expected[key], actual: undefined });
continue;
}
mismatches = mismatches.concat(await this.jsonComparison(expected[key], actual[key], subPath));
}
return mismatches;
}
}
function parseValue(value) {
if (value === 'true')
return true;
if (value === 'false')
return false;
if (!isNaN(Number(value)))
return Number(value);
return value;
}