UNPKG

tm-playwright-framework

Version:

Playwright Cucumber TS framework - The easiest way to learn

287 lines (286 loc) 13 kB
/** * 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; }