UNPKG

@decaf-ts/utils

Version:

module management utils for decaf-ts

444 lines 19.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestReporter = exports.TestReporterStoragePathEnvKey = exports.TestReporterStorageEnabledEnvKey = exports.JestReportersTempPathEnvKey = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const fs_js_1 = require("./../utils/fs.cjs"); /** * @description Environment variable key for Jest HTML reporters temporary directory path * @summary Constant defining the environment variable key for Jest HTML reporters * @const JestReportersTempPathEnvKey * @memberOf module:utils */ exports.JestReportersTempPathEnvKey = "JEST_HTML_REPORTERS_TEMP_DIR_PATH"; /** * @description Environment variable key to enable file-based evidence storage * @summary Constant defining the environment variable key for storage enablement * @const TestReporterStorageEnabledEnvKey * @memberOf module:utils */ exports.TestReporterStorageEnabledEnvKey = "TEST_REPORTER_STORAGE_ENABLED"; /** * @description Environment variable key for the base path of evidence storage * @summary Constant defining the environment variable key for storage path * @const TestReporterStoragePathEnvKey * @memberOf module:utils */ exports.TestReporterStoragePathEnvKey = "TEST_REPORTER_STORAGE_PATH"; /** * @description Array of dependencies required by the test reporter * @summary List of npm packages needed for reporting functionality * @const dependencies * @memberOf module:utils */ const dependencies = ["jest-html-reporters", "json2md", "chartjs-node-canvas"]; /** * @description Normalizes imports to handle both CommonJS and ESModule formats * @summary Utility function to handle module import differences between formats * @template T - Type of the imported module * @param {Promise<T>} importPromise - Promise returned by dynamic import * @return {Promise<T>} Normalized module * @function normalizeImport * @memberOf module:utils */ async function normalizeImport(importPromise) { // CommonJS's `module.exports` is wrapped as `default` in ESModule. return importPromise.then((m) => (m.default || m)); } /** * @description Test reporting utility class for managing test results and evidence * @summary A comprehensive test reporter that handles various types of test artifacts including messages, * attachments, data, images, tables, and graphs. It provides methods to report and store test evidence * in different formats and manages dependencies for reporting functionality. * * @template T - Type of data being reported * @param {string} [testCase="tests"] - Name of the test case * @param {string} [basePath] - Base path for storing test reports * @class * * @example * ```typescript * const reporter = new TestReporter('login-test'); * * // Report test messages * await reporter.reportMessage('Test Started', 'Login flow initiated'); * * // Report test data * await reporter.reportData('user-credentials', { username: 'test' }, 'json'); * * // Report test results table * await reporter.reportTable('test-results', { * headers: ['Step', 'Status'], * rows: [ * { Step: 'Login', Status: 'Pass' }, * { Step: 'Validation', Status: 'Pass' } * ] * }); * * // Report test evidence * await reporter.reportAttachment('Screenshot', screenshotBuffer); * ``` * * @mermaid * sequenceDiagram * participant Client * participant TestReporter * participant FileSystem * participant Dependencies * * Client->>TestReporter: new TestReporter(testCase, basePath) * TestReporter->>FileSystem: Create report directory * * alt Report Message * Client->>TestReporter: reportMessage(title, message) * TestReporter->>Dependencies: Import helpers * TestReporter->>FileSystem: Store message * else Report Data * Client->>TestReporter: reportData(reference, data, type) * TestReporter->>Dependencies: Process data * TestReporter->>FileSystem: Store formatted data * else Report Table * Client->>TestReporter: reportTable(reference, tableDef) * TestReporter->>Dependencies: Convert to MD format * TestReporter->>FileSystem: Store table * end */ class TestReporter { /** * @description Checks if storage is enabled via environment variables * @summary Static getter for storage enablement status * @type {boolean} */ static get storageEnabled() { return (process.env[exports.TestReporterStorageEnabledEnvKey] === "true" || process.env["TEST_REPORTER_STORE_EVIDENCE"] === "true"); } /** * @description Gets the base path for evidence storage from environment variables or default * @summary Static getter for evidence storage base path * @type {string} */ static get storagePath() { return (process.env[exports.TestReporterStoragePathEnvKey] || path_1.default.join(process.cwd(), "workdocs", "reports", "evidences")); } constructor(testCase = "tests", basePath = TestReporter.storagePath) { this.testCase = testCase; this.basePath = basePath; const parentPath = basePath; this.basePath = path_1.default.join(basePath, this.testCase); if (!fs_1.default.existsSync(this.basePath)) { fs_1.default.mkdirSync(parentPath, { recursive: true }); } } /** * @description Retrieves all evidences for a given describe and it name * @summary Searches the storage directory for matching evidences using string inclusion * @param {string} describeName - Name of the describe block to match * @param {string} itName - Name of the it block to match * @return {EvidenceData[]} Array of matching evidences */ static getEvidencesOf(describeName, itName) { const root = TestReporter.storagePath; if (!fs_1.default.existsSync(root)) return []; const evidences = []; const normalize = (s) => s.toLowerCase().replace(/[- ]/g, ""); const nDescribe = normalize(describeName); const nIt = normalize(itName); const describeDirs = fs_1.default .readdirSync(root) .filter((d) => normalize(d).includes(nDescribe)); for (const dDir of describeDirs) { const dPath = path_1.default.join(root, dDir); if (!fs_1.default.statSync(dPath).isDirectory()) continue; const itDirs = fs_1.default .readdirSync(dPath) .filter((i) => normalize(i).includes(nIt)); for (const iDir of itDirs) { const iPath = path_1.default.join(dPath, iDir); if (!fs_1.default.statSync(iPath).isDirectory()) continue; const files = fs_1.default.readdirSync(iPath); for (const file of files) { const filePath = path_1.default.join(iPath, file); if (fs_1.default.statSync(filePath).isFile()) { const content = fs_1.default.readFileSync(filePath); const isText = [".json", ".txt", ".md"].some((ext) => file.endsWith(ext)); evidences.push({ name: file, path: filePath, content: isText ? content.toString("utf-8") : content, }); } } } } return evidences; } /** * @description Imports required helper functions * @summary Ensures all necessary dependencies are available and imports helper functions * @return {Promise<void>} Promise that resolves when helpers are imported */ async importHelpers() { this.ensureJestHtmlReportersTempDirs(); this.deps = await (0, fs_js_1.installIfNotAvailable)([dependencies[0]], this.deps); // if (!process.env[JestReportersTempPathEnvKey]) // process.env[JestReportersTempPathEnvKey] = './workdocs/reports'; const helper = await normalizeImport(Promise.resolve(`${`${dependencies[0]}/helper`}`).then(s => __importStar(require(s)))); const { addMsg, addAttach } = helper; this.overrideJestHtmlReportersTempPaths(helper); TestReporter.addMsgFunction = addMsg; TestReporter.addAttachFunction = addAttach; } getJestHtmlReportersTempDir() { return (process.env[exports.JestReportersTempPathEnvKey] || path_1.default.join(this.basePath, "jest-html-reporters-temp")); } ensureJestHtmlReportersTempDirs() { const tempDir = this.getJestHtmlReportersTempDir(); fs_1.default.mkdirSync(path_1.default.join(tempDir, "data"), { recursive: true }); fs_1.default.mkdirSync(path_1.default.join(tempDir, "images"), { recursive: true }); } overrideJestHtmlReportersTempPaths(helper) { const tempDir = this.getJestHtmlReportersTempDir(); helper.tempDirPath = tempDir; helper.dataDirPath = path_1.default.join(tempDir, "data"); helper.attachDirPath = path_1.default.join(tempDir, "images"); } /** * @description Reports a message to the test report * @summary Adds a formatted message to the test report with an optional title * @param {string} title - Title of the message * @param {string | object} message - Content of the message * @return {Promise<void>} Promise that resolves when the message is reported */ async reportMessage(title, message) { if (!TestReporter.addMsgFunction) await this.importHelpers(); const msg = `${title}${message ? `\n${message}` : ""}`; await TestReporter.addMsgFunction({ message: msg }); } /** * @description Reports an attachment to the test report * @summary Adds a formatted message to the test report with an optional title * @param {string} title - Title of the message * @param {string | Buffer} attachment - Content of the message * @return {Promise<void>} Promise that resolves when the message is reported */ async reportAttachment(title, attachment) { if (!TestReporter.addAttachFunction) await this.importHelpers(); await TestReporter.addAttachFunction({ attach: attachment, description: title, }); } /** * @description Reports data with specified type * @summary Processes and stores data in the test report with formatting * @param {string} reference - Reference identifier for the data * @param {string | number | object} data - Data to be reported * @param {PayloadType} type - Type of the payload * @param {boolean} [trim=false] - Whether to trim the data * @return {Promise<void>} Promise that resolves when data is reported */ async report(reference, data, type, trim = false) { try { let attachFunction = this.reportMessage.bind(this); let extension = ".txt"; let finalData = data; switch (type) { case "image": finalData = Buffer.from(data); extension = ".png"; attachFunction = this.reportAttachment.bind(this); break; case "json": if (trim) { // we clone to avoid modifying the original if it was an object passed by reference const dataToTrim = JSON.parse(JSON.stringify(data)); if (dataToTrim.request) delete dataToTrim.request; if (dataToTrim.config) delete dataToTrim.config; finalData = dataToTrim; } finalData = JSON.stringify(finalData, null, 2); extension = ".json"; break; case "md": extension = ".md"; break; case "text": extension = ".txt"; break; default: console.log(`Unsupported type ${type}. assuming text`); } const refWithExt = reference.includes("\n") || reference.includes(extension) ? reference : `${reference}${extension}`; await attachFunction(refWithExt, finalData); if (TestReporter.storageEnabled) { await this.storeEvidence(reference, finalData, type, extension); } } catch (e) { throw new Error(`Could not store attach artifact ${reference} under to test report ${this.testCase} - ${e}`); } } /** * @description Stores evidence to a file in the specified directory structure * @summary Internal method to handle file-based storage of test artifacts * @param {string} reference - Reference identifier for the data * @param {any} data - Data to be stored * @param {PayloadType} type - Type of the payload * @param {string} extension - File extension to use * @return {Promise<void>} */ async storeEvidence(reference, data, type, extension) { let itName = "unknown-it"; try { const state = global.expect?.getState(); if (state?.currentTestName) { itName = state.currentTestName; if (itName.startsWith(this.testCase)) { itName = itName.substring(this.testCase.length).trim(); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore } const sanitizedItName = itName.replace(/[/\\?%*:|"<>]/g, "-") || "unknown-it"; const evidenceDir = path_1.default.join(this.basePath, sanitizedItName); if (!fs_1.default.existsSync(evidenceDir)) { fs_1.default.mkdirSync(evidenceDir, { recursive: true }); } const fileName = reference.includes(extension) ? reference : `${reference}${extension}`; const filePath = path_1.default.join(evidenceDir, fileName); if (type === "image" || Buffer.isBuffer(data)) { fs_1.default.writeFileSync(filePath, data); } else { fs_1.default.writeFileSync(filePath, typeof data === "string" ? data : JSON.stringify(data, null, 2)); } } /** * @description Reports data with a specified type * @summary Wrapper method for reporting various types of data * @param {string} reference - Reference identifier for the data * @param {string | number | object} data - Data to be reported * @param {PayloadType} [type="json"] - Type of the payload * @param {boolean} [trim=false] - Whether to trim the data * @return {Promise<void>} Promise that resolves when data is reported */ async reportData(reference, data, type = "json", trim = false) { return this.report(reference, data, type, trim); } /** * @description Reports a JSON object * @summary Convenience method for reporting JSON objects * @param {string} reference - Reference identifier for the object * @param {object} json - JSON object to be reported * @param {boolean} [trim=false] - Whether to trim the object * @return {Promise<void>} Promise that resolves when object is reported */ async reportObject(reference, json, trim = false) { return this.report(reference, json, "json", trim); } /** * @description Reports a table in markdown format * @summary Converts and stores a table definition in markdown format * @param {string} reference - Reference identifier for the table * @param {MdTableDefinition} tableDef - Table definition object * @return {Promise<void>} Promise that resolves when table is reported */ async reportTable(reference, tableDef) { this.deps = await (0, fs_js_1.installIfNotAvailable)([dependencies[1]], this.deps); let txt; try { const json2md = await normalizeImport(Promise.resolve(`${`${dependencies[1]}`}`).then(s => __importStar(require(s)))); txt = json2md([{ table: tableDef }]); } catch (e) { throw new Error(`Could not convert JSON to Markdown - ${e}`); } return this.report(reference, txt, "md"); } /** * @description Reports a graph using Chart.js * @summary Generates and stores a graph visualization * @param {string} reference - Reference identifier for the graph * @param {any} config - Chart.js configuration object * @return {Promise<void>} Promise that resolves when graph is reported */ async reportGraph(reference, config, width = 1200, height = 600) { this.deps = await (0, fs_js_1.installIfNotAvailable)([dependencies[2]], this.deps); const { ChartJSNodeCanvas } = await normalizeImport(Promise.resolve(`${dependencies[2]}`).then(s => __importStar(require(s)))); const backgroundColour = "white"; // Uses https://www.w3schools.com/tags/canvas_fillstyle.asp const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour, }); const image = await chartJSNodeCanvas.renderToBuffer(config); await this.reportImage(reference, image); return image; } /** * @description Reports an image to the test report * @summary Stores an image buffer in the test report * @param {string} reference - Reference identifier for the image * @param {Buffer} buffer - Image data buffer * @return {Promise<void>} Promise that resolves when image is reported */ async reportImage(reference, buffer) { return this.report(reference, buffer, "image"); } } exports.TestReporter = TestReporter; //# sourceMappingURL=TestReporter.js.map //# sourceMappingURL=TestReporter.cjs.map