UNPKG

chappie-runner

Version:
297 lines (290 loc) 10.7 kB
import { readFile } from "fs"; import chalk from "chalk"; import superagent from "superagent"; import { Reporter } from "./utils/reports.js"; import { compareImages } from "./utils/image.js"; import { prepareFolders } from "./utils/files.js"; import { parseJSONTest, transformTests } from "./utils/tests.js"; const ACTUAL_IMAGES_FOLDER = "./actual-images"; const BASE_IMAGES_FOLDER = "./base-images"; const DIFF_IMAGES_FOLDER = "./diff-images"; const initialOutputJson = { summary: { iterations: 0, concurrency: 1, totalRequests: 0, passedRequests: 0, failedRequests: 0, totalAssertions: 0, passedAssertions: 0, failedAssertions: 0, totalTests: 0, passedTests: 0, failedTests: 0, }, results: [], }; /** * Executes a series of tests defined in a collection file and generates a report. * * @async * @function * @param {Object} testConfig - Configuration object for the test run. * @param {string} testConfig.collection - Path to the collection file containing test definitions. * @param {string} [testConfig.actualImagesFolder=ACTUAL_IMAGES_FOLDER] - Folder to store actual images. * @param {string} [testConfig.baseImagesFolder=BASE_IMAGES_FOLDER] - Folder containing base images for comparison. * @param {string} [testConfig.diffImagesFolder=DIFF_IMAGES_FOLDER] - Folder to store diff images. * @param {number} [testConfig.concurrency=1] - Number of concurrent tests to run. * @param {number} [testConfig.iterations=1] - Number of iterations to run the tests. * @param {Function} [testConfig.onBeforeRequest] - Callback executed before each request. * @param {Function} [testConfig.onAfterRequest] - Callback executed after each request. * @param {Function} [testConfig.onRequestError] - Callback executed when a request fails. * @param {Function} [testConfig.onTestError] - Callback executed when a test fails. * @param {Object} [testConfig.reporter] - Configuration for the test reporter. * @param {boolean} [testConfig.reporter.enabled=false] - Whether the reporter is enabled. * @param {string} [testConfig.reporter.name="junit"] - Name of the reporter. * @param {string} [testConfig.reporter.output="./reports"] - Output directory for the reporter. * @returns {Promise<Object>} A promise that resolves to the test results summary and details. * * @throws {Error} If there is an issue reading the collection file or during test execution. * * @example * const testConfig = { * collection: './path/to/collection.json', * concurrency: 2, * iterations: 3, * reporter: { * enabled: true, * name: 'junit', * output: './test-reports', * }, * onBeforeRequest: (request) => console.log('Before request:', request), * onAfterRequest: (response) => console.log('After request:', response), * onRequestError: (error) => console.error('Request error:', error), * }; * * run(testConfig) * .then((results) => console.log('Test results:', results)) * .catch((error) => console.error('Error during test run:', error)); */ const run = async (testConfig) => { const outputJson = { summary: { ...initialOutputJson.summary }, results: [] }; return new Promise((resolve, reject) => { try { const { collection, actualImagesFolder = ACTUAL_IMAGES_FOLDER, baseImagesFolder = BASE_IMAGES_FOLDER, diffImagesFolder = DIFF_IMAGES_FOLDER, concurrency = 1, iterations = 1, onBeforeRequest, onAfterRequest, onRequestError, onTestError, reporter = { enabled: false, name: "junit", output: "./reports", }, } = testConfig; outputJson.summary.concurrency = concurrency; let hasInstantiatedImages = false; let startTime; const completeReport = () => { if (!reporter.enabled) { return; } Reporter.completeReport({ outputJson, reporter, }); }; const reportResults = (folder, name, assertionError) => { if (!reporter.enabled) { return; } Reporter.saveResult({ assertionError, folder, name, outputJson, reporter, startTime, }); }; readFile(collection, "utf8", async (err, data) => { if (err) { console.error("Could not list the directory.", err); process.exit(1); } const parsedData = JSON.parse(data); let folderName = ""; async function traverseBrunoCollection(items) { const sortedItems = items.sort((a, b) => a.seq - b.seq); for (const item of sortedItems) { if (item.type === "folder") { folderName = item.name; await traverseBrunoCollection(item.items); } else { const { url, method, params, body: { json }, tests, headers, } = item.request; const folder = folderName.length ? `${folderName}-` : ""; const testName = `${folder}${item.name}` .replaceAll("/", "-") .replaceAll(" ", "-") .toLowerCase(); console.log(chalk.bgCyan(`Running test: ${testName}`)); let parsedTests = transformTests(tests); const options = { method, data: method === "POST" ? json : undefined, headers: headers.reduce((acc, header) => { if (header.enabled) { acc[header.name] = header.value; } return acc; }, {}), }; if (method === "POST") { options.headers["Content-Type"] = "application/json"; } let fullUrl = new URL(url); fullUrl = `${fullUrl.origin}${fullUrl.pathname}`; const queryParams = new URLSearchParams(); params.forEach((param) => { const { enabled, name, value } = param; if (enabled && !queryParams.has(name)) { queryParams.append(name, value); } }); if (queryParams.toString().length) { fullUrl = `${fullUrl}?${queryParams.toString()}`; } const superagentRequest = superagent[method.toLowerCase()]( fullUrl, ).set(options.headers); if (method === "POST") { superagentRequest.send(json); } const generateTestResponse = (res) => ({ status: res.status, data: res.type === "text/html" ? res.text : res.body, headers: res.headers, }); outputJson.summary.totalRequests++; startTime = Date.now(); onBeforeRequest?.(superagentRequest); const response = await superagentRequest.catch((err) => { onRequestError?.(err); if (tests) { try { parseJSONTest( parsedTests, generateTestResponse(err.response), outputJson, ); reportResults(folderName, item.name); } catch (assertionError) { onTestError?.(assertionError); outputJson.summary.failedRequests++; reportResults(folderName, item.name, assertionError); } } return; }); if (!response) { continue; } onAfterRequest?.(response); const contentType = response.headers["content-type"]; if (tests) { try { parseJSONTest( parsedTests, generateTestResponse(response), outputJson, ); reportResults(folderName, item.name); } catch (assertionError) { onTestError?.(assertionError); reportResults(folderName, item.name, assertionError); } } if (contentType.startsWith("image/")) { if (!hasInstantiatedImages) { hasInstantiatedImages = true; prepareFolders({ baseImagesFolder, actualImagesFolder, diffImagesFolder, }); } if (response.status !== 200) { console.log( chalk.bgCyan( `Expected status is not 200, skipping image comparison`, ), ); return; } try { await compareImages({ response, item, testName, actualImagesFolder, baseImagesFolder, diffImagesFolder, }); reportResults(folderName, item.name); } catch (assertionError) { onTestError?.(assertionError); reportResults(folderName, item.name, assertionError); console.log( chalk.red(`Actual image saved at ${actualImagesFolder}`), ); console.log( chalk.bgRed(`Diff image saved at ${diffImagesFolder}`), ); if (reporter.enabled) { Reporter.saveResult({ assertionError, folder: folderName, name: item.name, outputJson, reporter, startTime, }); } } } } } } const testsToRun = Array.from({ length: iterations }); for (const _ of testsToRun) { const concurrentTests = Array.from({ length: concurrency }, () => traverseBrunoCollection(parsedData.items), ); await Promise.all(concurrentTests); outputJson.summary.iterations++; } completeReport(); console.table(outputJson.summary); resolve(outputJson); }); } catch (error) { reject(error); } }); }; export const chappie = { run, };