UNPKG

odhin-reports-playwright

Version:
880 lines (879 loc) 61.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const help_1 = __importDefault(require("./help")); const picocolors_1 = require("picocolors"); const lodash_1 = require("lodash"); const child_process_1 = require("child_process"); const node_util_1 = __importDefault(require("node:util")); const process_1 = require("process"); class Generate { constructor(execOptions) { // Constants this.OUTPUT_FOLDER = "odhin-reports"; this.TITLE = "Odhin Reports"; this.TEST_FOLDER = "tests"; this.THEME = "light"; this.INDEX_FILE = "index.html"; this.CURRENT_VERSION = "1.1.8"; this.execOptions = execOptions; this.totalDuration = 0; this.help = new help_1.default(this.execOptions); this.testCases = []; this.execInfo = { projectName: "", totalPassed: 0, totalFailed: 0, totalTimedOut: 0, totalSkipped: 0, totalInterrupted: 0, totalFlaky: 0, totalTests: 0, startTime: "", endTime: "", execFiles: [], execProjects: [], }; this.execOptions.testFolder = this.execOptions.testFolder !== undefined ? this.execOptions.testFolder : this.TEST_FOLDER; this.execOptions.title = this.execOptions.title !== undefined ? this.execOptions.title : this.TITLE; this.execOptions.testEnvironment = this.execOptions.testEnvironment !== undefined ? this.execOptions.testEnvironment : ""; this.execOptions.project = this.execOptions.project !== undefined ? this.execOptions.project : ""; this.execOptions.release = this.execOptions.release !== undefined ? this.execOptions.release : ""; this.execOptions.embedAssets = this.execOptions.embedAssets !== undefined ? this.execOptions.embedAssets : true; this.execOptions.embedAttachments = this.execOptions.embedAttachments !== undefined ? this.execOptions.embedAttachments : true; this.execOptions.outputFolder = this.execOptions.outputFolder !== undefined ? this.execOptions.outputFolder : this.OUTPUT_FOLDER; this.execOptions.startServer = this.execOptions.startServer !== undefined ? this.execOptions.startServer : true; this.execOptions.consoleLog = this.execOptions.consoleLog !== undefined ? this.execOptions.consoleLog : true; this.execOptions.simpleConsoleLog = this.execOptions.simpleConsoleLog !== undefined ? this.execOptions.simpleConsoleLog : false; this.execOptions.consoleError = this.execOptions.consoleError !== undefined ? this.execOptions.consoleError : true; this.execOptions.testOutput = this.execOptions.testOutput !== undefined ? this.execOptions.testOutput : "only-on-failure"; this.execOptions.consoleTestOutput = this.execOptions.consoleTestOutput !== undefined ? this.execOptions.consoleTestOutput : false; this.execOptions.initialTheme = this.execOptions.initialTheme !== undefined ? this.execOptions.initialTheme : this.THEME; this.execOptions.indexFilename = this.execOptions.indexFilename !== undefined ? this.execOptions.indexFilename .replaceAll(".html", "") .replaceAll(".htm", "") .replaceAll(".", "_") .replaceAll(" ", "_") + ".html" : this.INDEX_FILE; this.execOptions.testGroupOnConsole = this.execOptions.testGroupOnConsole !== undefined ? this.execOptions.testGroupOnConsole : true; this.execOptions.testProjectOnConsole = this.execOptions.testProjectOnConsole !== undefined ? this.execOptions.testProjectOnConsole : true; this.execOptions.testListColumns = this.execOptions.testListColumns !== undefined ? String(this.execOptions.testListColumns).toUpperCase().split(",") : []; this.help.showError = this.execOptions.consoleError; // Remove output folder if exists fs_1.default.rmSync(this.execOptions.outputFolder, { recursive: true, force: true }); try { // Load HTML Files fs_1.default.readFile(path_1.default.resolve(__dirname, "html/base.html"), "utf-8", (err, data) => { if (err) throw err; this.base = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/runInfo.html"), "utf-8", (err, data) => { if (err) throw err; this.runInfo = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/chart.html"), "utf-8", (err, data) => { if (err) throw err; this.chart = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/chartInfo.html"), "utf-8", (err, data) => { if (err) throw err; this.chartInfo = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/chartStatus.html"), "utf-8", (err, data) => { if (err) throw err; this.chartStatus = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/chartFile.html"), "utf-8", (err, data) => { if (err) throw err; this.chartFile = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/chartProject.html"), "utf-8", (err, data) => { if (err) throw err; this.chartProject = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/chartJs.html"), "utf-8", (err, data) => { if (err) throw err; this.chartJs = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/content.html"), "utf-8", (err, data) => { if (err) throw err; this.content = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/summary.html"), "utf-8", (err, data) => { if (err) throw err; this.summary = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/summaryRow.html"), "utf-8", (err, data) => { if (err) throw err; this.summaryRow = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/testCase.html"), "utf-8", (err, data) => { if (err) throw err; this.testCaseTemplate = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/testCaseDetails.html"), "utf-8", (err, data) => { if (err) throw err; this.testCaseDetailsTemplate = data; }); // Load Theme Files if (fs_1.default.existsSync(this.execOptions.initialTheme)) { fs_1.default.readFile(this.execOptions.initialTheme, "utf-8", (err, data) => { if (err) throw err; this.cssThemeCustom = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/customTheme.min.js"), "utf-8", (err, data) => { if (err) throw err; this.themeJs = data; }); } else { this.cssThemeCustom = ""; switch (this.execOptions.initialTheme) { case "light": fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/lightTheme.min.js"), "utf-8", (err, data) => { if (err) throw err; this.themeJs = data; }); break; case "dark": fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/darkTheme.min.js"), "utf-8", (err, data) => { if (err) throw err; this.themeJs = data; }); break; default: fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/lightTheme.min.js"), "utf-8", (err, data) => { if (err) throw err; this.themeJs = data; }); break; } } fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/light-theme.min.css"), "utf-8", (err, data) => { if (err) throw err; this.cssThemeLight = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/dark-theme.min.css"), "utf-8", (err, data) => { if (err) throw err; this.cssThemeDark = data; }); // Load CSS Files fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/style-colors.min.css"), "utf-8", (err, data) => { if (err) throw err; this.cssColors = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/style.min.css"), "utf-8", (err, data) => { if (err) throw err; this.css = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/jquery.dataTables.min.css"), "utf-8", (err, data) => { if (err) throw err; this.jqueryDataTablesCss = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/bootstrap.min.css"), "utf-8", (err, data) => { if (err) throw err; this.bootstrapCss = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/prism.min.css"), "utf-8", (err, data) => { if (err) throw err; this.prismCss = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/css/img-comparison-slider.min.css"), "utf-8", (err, data) => { if (err) throw err; this.imgComparisonSliderCss = data; }); // Load JS Files fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/jquery-3.5.1.min.js"), "utf-8", (err, data) => { if (err) throw err; this.jQuery = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/script.min.js"), "utf-8", (err, data) => { if (err) throw err; this.js = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/chart.min.js"), "utf-8", (err, data) => { if (err) throw err; this.chartJsLibrary = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/jquery.dataTables.min.js"), "utf-8", (err, data) => { if (err) throw err; this.jqueryDataTablesJs = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/dataTables.min.js"), "utf-8", (err, data) => { if (err) throw err; this.dataTables = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/bootstrap.min.js"), "utf-8", (err, data) => { if (err) throw err; this.bootstrapJs = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/prism.min.js"), "utf-8", (err, data) => { if (err) throw err; this.prismJs = data; }); fs_1.default.readFile(path_1.default.resolve(__dirname, "html/assets/js/img-comparison-slider.min.js"), "utf-8", (err, data) => { if (err) throw err; this.imgComparisonSliderJs = data; }); } catch (error) { if (this.execOptions.consoleError) { console.error(error); } } if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`-------------------------------------`); console.log(` `); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.blue)(`-------------------------------------`))}`); console.log(`${(0, picocolors_1.bold)((0, picocolors_1.blue)(` `))}`); } } } async onBegin(config, suite) { this.execInfo.startTime = new Date().toUTCString(); config.projects.forEach((p, index) => { this.execInfo.projectName += index > 0 ? ", " : ""; this.execInfo.projectName += p.name.charAt(0).toUpperCase() + p.name.slice(1); }); if (suite.allTests().length >= 1) { if (this.execOptions.consoleLog) { let text = `Starting the run with ${suite.allTests().length} test`; if (suite.allTests().length > 1) { text += "s"; } if (this.execOptions.simpleConsoleLog) { console.log(`⏺ ` + text); console.log(` `); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.yellow)(`⏺ `))}${(0, picocolors_1.bold)((0, picocolors_1.blue)(text))}`); console.log(`${(0, picocolors_1.bold)((0, picocolors_1.blue)(` `))}`); } } } else { if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`🔴 No tests found`); console.log(` `); console.log(`-------------------------------------`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.red)(`🔴 No tests found`))}`); console.log(`${(0, picocolors_1.bold)((0, picocolors_1.blue)(` `))}`); console.log(`${(0, picocolors_1.bold)((0, picocolors_1.blue)(`-------------------------------------`))}`); } } (0, process_1.exit)(1); } } async onTestEnd(testCase, result) { this.execInfo.endTime = new Date().toUTCString(); this.testCases.push({ testCase: testCase, result: result, }); this.totalDuration = this.totalDuration + result.duration; let project = ""; let tst = testCase; if (tst.projectId !== undefined) { project = tst.projectId; project = project.charAt(0).toUpperCase() + project.slice(1) + " | "; } if (result.status === "passed" && result.retry > 0) { result.status = "flaky"; } if (result.status === "passed" || result.status === "flaky" || result.retry === testCase.retries) { // Update Files let fileUpdate = false; this.execInfo.execFiles.forEach((f, index) => { if (f.filename === this.help.getShortFilePath(testCase.filename, this.execOptions.testFolder)) { this.execInfo.execFiles[index].totalTests++; this.execInfo.execFiles[index].totalDuration = this.execInfo.execFiles[index].totalDuration + result.duration; (this.execInfo.execFiles[index].totalPassed = result.status == "passed" ? this.execInfo.execFiles[index].totalPassed + 1 : this.execInfo.execFiles[index].totalPassed), (this.execInfo.execFiles[index].totalFailed = result.status == "failed" ? this.execInfo.execFiles[index].totalFailed + 1 : this.execInfo.execFiles[index].totalFailed), (this.execInfo.execFiles[index].totalTimedOut = result.status == "timedOut" ? this.execInfo.execFiles[index].totalTimedOut + 1 : this.execInfo.execFiles[index].totalTimedOut), (this.execInfo.execFiles[index].totalSkipped = result.status == "skipped" ? this.execInfo.execFiles[index].totalSkipped + 1 : this.execInfo.execFiles[index].totalSkipped), (this.execInfo.execFiles[index].totalInterrupted = result.status == "interrupted" ? this.execInfo.execFiles[index].totalInterrupted + 1 : this.execInfo.execFiles[index].totalInterrupted), (this.execInfo.execFiles[index].totalFlaky = result.status == "flaky" ? this.execInfo.execFiles[index].totalFlaky + 1 : this.execInfo.execFiles[index].totalFlaky), (this.execInfo.execFiles[index].endTime = new Date().toUTCString()); fileUpdate = true; } }); if (!fileUpdate) { this.execInfo.execFiles.push({ filename: this.help.getShortFilePath(testCase.filename, this.execOptions.testFolder), color: this.help.getRandomColor(), totalPassed: result.status == "passed" ? 1 : 0, totalFailed: result.status == "failed" ? 1 : 0, totalTimedOut: result.status == "timedOut" ? 1 : 0, totalSkipped: result.status == "skipped" ? 1 : 0, totalInterrupted: result.status == "interrupted" ? 1 : 0, totalFlaky: result.status == "flaky" ? 1 : 0, totalTests: 1, totalDuration: result.duration, startTime: new Date(new Date().getTime() - result.duration).toUTCString(), endTime: new Date().toUTCString(), }); } // Update Projects let projectUpdate = false; this.execInfo.execProjects.forEach((b, index) => { if (b.project === testCase.projectId) { this.execInfo.execProjects[index].totalTests++; this.execInfo.execProjects[index].totalDuration = this.execInfo.execProjects[index].totalDuration + result.duration; (this.execInfo.execProjects[index].totalPassed = result.status == "passed" ? this.execInfo.execProjects[index].totalPassed + 1 : this.execInfo.execProjects[index].totalPassed), (this.execInfo.execProjects[index].totalFailed = result.status == "failed" ? this.execInfo.execProjects[index].totalFailed + 1 : this.execInfo.execProjects[index].totalFailed), (this.execInfo.execProjects[index].totalTimedOut = result.status == "timedOut" ? this.execInfo.execProjects[index].totalTimedOut + 1 : this.execInfo.execProjects[index].totalTimedOut), (this.execInfo.execProjects[index].totalSkipped = result.status == "skipped" ? this.execInfo.execProjects[index].totalSkipped + 1 : this.execInfo.execProjects[index].totalSkipped), (this.execInfo.execProjects[index].totalInterrupted = result.status == "interrupted" ? this.execInfo.execProjects[index].totalInterrupted + 1 : this.execInfo.execProjects[index].totalInterrupted), (this.execInfo.execProjects[index].totalFlaky = result.status == "flaky" ? this.execInfo.execProjects[index].totalFlaky + 1 : this.execInfo.execProjects[index].totalFlaky), (this.execInfo.execProjects[index].endTime = new Date().toUTCString()); projectUpdate = true; } }); if (!projectUpdate && testCase.projectInfo) { this.execInfo.execProjects.push({ project: testCase.projectId, color: this.help.getRandomColor(), totalPassed: result.status == "passed" ? 1 : 0, totalFailed: result.status == "failed" ? 1 : 0, totalTimedOut: result.status == "timedOut" ? 1 : 0, totalSkipped: result.status == "skipped" ? 1 : 0, totalInterrupted: result.status == "interrupted" ? 1 : 0, totalFlaky: result.status == "flaky" ? 1 : 0, totalTests: 1, totalDuration: result.duration, startTime: new Date(new Date().getTime() - result.duration).toUTCString(), endTime: new Date().toUTCString(), }); } let groupName = ""; if (this.execOptions.testGroupOnConsole && testCase.title !== testCase.titlePath[3]) { groupName = `${testCase.title !== testCase.titlePath[3] ? testCase.titlePath[3] : ""} | `; } let testTitle = `${groupName}${testCase.title}`; if (this.execOptions.testProjectOnConsole) { testTitle = `${project}${testTitle}`; } switch (result.status) { case "passed": this.execInfo.totalPassed++; if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`✅ ${testTitle}`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.green)(`✅ ${testTitle}`))}`); } } break; case "failed": this.execInfo.totalFailed++; if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`❌ ${testTitle}`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.red)(`❌ ${testTitle}`))}`); } } break; case "timedOut": this.execInfo.totalTimedOut++; if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`⛔ ${testTitle}`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.cyan)(`⛔ ${testTitle}`))}`); } } break; case "skipped": this.execInfo.totalSkipped++; if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`🚫 ${testTitle}`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.yellow)(`🚫 ${testTitle}`))}`); } } break; case "interrupted": this.execInfo.totalInterrupted++; if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`⛔ ${testTitle}`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.gray)(`⛔ ${testTitle}`))}`); } } break; case "flaky": this.execInfo.totalFlaky++; if (this.execOptions.consoleLog) { if (this.execOptions.simpleConsoleLog) { console.log(`❎ ${testTitle}`); } else { console.log(`${(0, picocolors_1.bold)((0, picocolors_1.magenta)(`❎ ${testTitle}`))}`); } } break; } this.execInfo.totalTests++; } } async onEnd() { // Edit Chart Status this.chartStatus = this.chartStatus.replace("{{.ChartId}}", "chart-status"); this.chartStatus = this.chartStatus.replace("{{.TotalTests}}", this.execInfo.totalTests.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestPassedValue}}", this.execInfo.totalPassed.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestFailedValue}}", this.execInfo.totalFailed.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestTimedOutValue}}", this.execInfo.totalTimedOut.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestSkippedValue}}", this.execInfo.totalSkipped.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestInterruptedValue}}", this.execInfo.totalInterrupted.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestFlakyValue}}", this.execInfo.totalFlaky.toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestPassedPercentage}}", (0, lodash_1.round)((this.execInfo.totalPassed / this.execInfo.totalTests) * 100, 2).toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestFailedPercentage}}", (0, lodash_1.round)((this.execInfo.totalFailed / this.execInfo.totalTests) * 100, 2).toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestTimedOutPercentage}}", (0, lodash_1.round)((this.execInfo.totalTimedOut / this.execInfo.totalTests) * 100, 2).toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestSkippedPercentage}}", (0, lodash_1.round)((this.execInfo.totalSkipped / this.execInfo.totalTests) * 100, 2).toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestInterruptedPercentage}}", (0, lodash_1.round)((this.execInfo.totalInterrupted / this.execInfo.totalTests) * 100, 2).toString()); this.chartStatus = this.chartStatus.replaceAll("{{.TestFlakyPercentage}}", (0, lodash_1.round)((this.execInfo.totalFlaky / this.execInfo.totalTests) * 100, 2).toString()); this.chartJs = this.chartJs.replaceAll("{{.PassedStatusChart}}", this.execInfo.totalPassed.toString()); this.chartJs = this.chartJs.replaceAll("{{.FailedStatusChart}}", this.execInfo.totalFailed.toString()); this.chartJs = this.chartJs.replaceAll("{{.TimedOutStatusChart}}", this.execInfo.totalTimedOut.toString()); this.chartJs = this.chartJs.replaceAll("{{.SkippedStatusChart}}", this.execInfo.totalSkipped.toString()); this.chartJs = this.chartJs.replaceAll("{{.InterruptedStatusChart}}", this.execInfo.totalInterrupted.toString()); this.chartJs = this.chartJs.replaceAll("{{.FlakyStatusChart}}", this.execInfo.totalFlaky.toString()); // Edit Chart File let chartFileDetail = this.chart; let chartJsFileLabels = ""; let chartJsFileData = ""; let chartJsFileDataColor = ""; let chartFileRows = ""; chartFileDetail = chartFileDetail.replace("{{.ChartId}}", "chart-file"); chartFileDetail = chartFileDetail.replace("{{.TotalTests}}", this.execInfo.totalTests.toString()); chartFileDetail = chartFileDetail.replace("{{.RowSpan}}", this.execInfo.execFiles.length.toString()); this.execInfo.execFiles.forEach((file, index) => { let chartRow = this.chartInfo; if (index == 0) { chartRow = chartRow.replace("{{.Label}}", file.filename); chartRow = chartRow.replace("{{.Value}}", file.totalTests.toString()); chartRow = chartRow.replace("{{.Percentage}}", (0, lodash_1.round)((file.totalTests / this.execInfo.totalTests) * 100, 2).toString()); chartJsFileLabels += "'" + file.filename + " (" + file.totalTests.toString() + ")" + "'"; chartJsFileData += file.totalTests.toString(); chartJsFileDataColor += "'" + file.color + "'"; } else { chartRow = chartRow.replace("{{.Label}}", file.filename); chartRow = chartRow.replace("{{.Value}}", file.totalTests.toString()); chartRow = chartRow.replace("{{.Percentage}}", (0, lodash_1.round)((file.totalTests / this.execInfo.totalTests) * 100, 2).toString()); chartJsFileLabels += ",'" + file.filename + " (" + file.totalTests.toString() + ")" + "'"; chartJsFileData += "," + file.totalTests.toString(); chartJsFileDataColor += ",'" + file.color + "'"; } chartRow = chartRow.replaceAll("{{.Color}}", file.color); chartFileRows += chartRow; }); this.chartFile = this.chartFile.replace("{{.Chart}}", chartFileDetail); this.chartFile = this.chartFile.replaceAll("{{.ChartFile}}", chartFileRows); this.chartJs = this.chartJs.replaceAll("{{.FilesChartLabels}}", chartJsFileLabels); this.chartJs = this.chartJs.replaceAll("{{.FilesChartData}}", chartJsFileData); this.chartJs = this.chartJs.replaceAll("{{.FilesChartDataColor}}", chartJsFileDataColor); // Edit Chart Project let chartProjectDetail = this.chart; let chartJsProjectLabels = ""; let chartJsProjectData = ""; let chartJsProjectDataColor = ""; let chartProjectRows = ""; chartProjectDetail = chartProjectDetail.replace("{{.ChartId}}", "chart-project"); chartProjectDetail = chartProjectDetail.replace("{{.TotalTests}}", this.execInfo.totalTests.toString()); chartProjectDetail = chartProjectDetail.replace("{{.RowSpan}}", this.execInfo.execFiles.length.toString()); this.execInfo.execProjects.forEach((project, index) => { let chartRow = this.chartInfo; if (index == 0) { chartRow = chartRow.replace("{{.Label}}", project.project); chartRow = chartRow.replace("{{.Value}}", project.totalTests.toString()); chartRow = chartRow.replace("{{.Percentage}}", (0, lodash_1.round)((project.totalTests / this.execInfo.totalTests) * 100, 2).toString()); chartJsProjectLabels += "'" + project.project + " (" + project.totalTests.toString() + ")" + "'"; chartJsProjectData += project.totalTests.toString(); chartJsProjectDataColor += "'" + project.color + "'"; } else { chartRow = chartRow.replace("{{.Label}}", project.project); chartRow = chartRow.replace("{{.Value}}", project.totalTests.toString()); chartRow = chartRow.replace("{{.Percentage}}", (0, lodash_1.round)((project.totalTests / this.execInfo.totalTests) * 100, 2).toString()); chartJsProjectLabels += ",'" + project.project + " (" + project.totalTests.toString() + ")" + "'"; chartJsProjectData += "," + project.totalTests.toString(); chartJsProjectDataColor += ",'" + project.color + "'"; } chartRow = chartRow.replaceAll("{{.Color}}", project.color); chartProjectRows += chartRow; }); this.chartProject = this.chartProject.replace("{{.Chart}}", chartProjectDetail); this.chartProject = this.chartProject.replaceAll("{{.ChartProject}}", chartProjectRows); this.chartJs = this.chartJs.replaceAll("{{.ProjectsChartLabels}}", chartJsProjectLabels); this.chartJs = this.chartJs.replaceAll("{{.ProjectsChartData}}", chartJsProjectData); this.chartJs = this.chartJs.replaceAll("{{.ProjectsChartDataColor}}", chartJsProjectDataColor); // Edit Summary Files let summaryFiles = this.summary; summaryFiles = summaryFiles.replace("{{.SummaryTitleName}}", "Status by test file"); summaryFiles = summaryFiles.replace("{{.SummaryColumnName}}", "Filename"); let filesSummaryRows = ""; this.execInfo.execFiles.forEach((file) => { let filesSummaryRow = this.summaryRow; filesSummaryRow = filesSummaryRow.replace("{{.Name}}", file.filename); filesSummaryRow = filesSummaryRow.replace("{{.TotalTests}}", file.totalTests.toString()); filesSummaryRow = filesSummaryRow.replace("{{.TotalExecutionTime}}", this.help.convertMsToTime(new Date(file.endTime).getTime() - new Date(file.startTime).getTime())); filesSummaryRow = filesSummaryRow.replace("{{.PassedValue}}", file.totalPassed.toString()); filesSummaryRow = filesSummaryRow.replace("{{.PassedPercentage}}", (0, lodash_1.round)((file.totalPassed / file.totalTests) * 100, 2).toString()); filesSummaryRow = filesSummaryRow.replace("{{.FailedValue}}", file.totalFailed.toString()); filesSummaryRow = filesSummaryRow.replace("{{.FailedPercentage}}", (0, lodash_1.round)((file.totalFailed / file.totalTests) * 100, 2).toString()); filesSummaryRow = filesSummaryRow.replace("{{.TimedOutValue}}", file.totalTimedOut.toString()); filesSummaryRow = filesSummaryRow.replace("{{.TimedOutPercentage}}", (0, lodash_1.round)((file.totalTimedOut / file.totalTests) * 100, 2).toString()); filesSummaryRow = filesSummaryRow.replace("{{.SkippedValue}}", file.totalSkipped.toString()); filesSummaryRow = filesSummaryRow.replace("{{.SkippedPercentage}}", (0, lodash_1.round)((file.totalSkipped / file.totalTests) * 100, 2).toString()); filesSummaryRow = filesSummaryRow.replace("{{.InterruptedValue}}", file.totalInterrupted.toString()); filesSummaryRow = filesSummaryRow.replace("{{.InterruptedPercentage}}", (0, lodash_1.round)((file.totalInterrupted / file.totalTests) * 100, 2).toString()); filesSummaryRow = filesSummaryRow.replace("{{.FlakyValue}}", file.totalFlaky.toString()); filesSummaryRow = filesSummaryRow.replace("{{.FlakyPercentage}}", (0, lodash_1.round)((file.totalFlaky / file.totalTests) * 100, 2).toString()); filesSummaryRows += filesSummaryRow; }); summaryFiles = summaryFiles.replace("{{.Data}}", filesSummaryRows); // Edit Summary Projects let summaryProjects = this.summary; summaryProjects = summaryProjects.replace("{{.SummaryTitleName}}", "Status by project"); summaryProjects = summaryProjects.replace("{{.SummaryColumnName}}", "Project"); let projectsSummaryRows = ""; this.execInfo.execProjects.forEach((project) => { let projectSummaryRow = this.summaryRow; projectSummaryRow = projectSummaryRow.replace("{{.Name}}", project.project); projectSummaryRow = projectSummaryRow.replace("{{.TotalTests}}", project.totalTests.toString()); projectSummaryRow = projectSummaryRow.replace("{{.TotalExecutionTime}}", this.help.convertMsToTime(new Date(project.endTime).getTime() - new Date(project.startTime).getTime())); projectSummaryRow = projectSummaryRow.replace("{{.PassedValue}}", project.totalPassed.toString()); projectSummaryRow = projectSummaryRow.replace("{{.PassedPercentage}}", (0, lodash_1.round)((project.totalPassed / project.totalTests) * 100, 2).toString()); projectSummaryRow = projectSummaryRow.replace("{{.FailedValue}}", project.totalFailed.toString()); projectSummaryRow = projectSummaryRow.replace("{{.FailedPercentage}}", (0, lodash_1.round)((project.totalFailed / project.totalTests) * 100, 2).toString()); projectSummaryRow = projectSummaryRow.replace("{{.TimedOutValue}}", project.totalTimedOut.toString()); projectSummaryRow = projectSummaryRow.replace("{{.TimedOutPercentage}}", (0, lodash_1.round)((project.totalTimedOut / project.totalTests) * 100, 2).toString()); projectSummaryRow = projectSummaryRow.replace("{{.SkippedValue}}", project.totalSkipped.toString()); projectSummaryRow = projectSummaryRow.replace("{{.SkippedPercentage}}", (0, lodash_1.round)((project.totalSkipped / project.totalTests) * 100, 2).toString()); projectSummaryRow = projectSummaryRow.replace("{{.InterruptedValue}}", project.totalInterrupted.toString()); projectSummaryRow = projectSummaryRow.replace("{{.InterruptedPercentage}}", (0, lodash_1.round)((project.totalInterrupted / project.totalTests) * 100, 2).toString()); projectSummaryRow = projectSummaryRow.replace("{{.FlakyValue}}", project.totalFlaky.toString()); projectSummaryRow = projectSummaryRow.replace("{{.FlakyPercentage}}", (0, lodash_1.round)((project.totalFlaky / project.totalTests) * 100, 2).toString()); projectsSummaryRows += projectSummaryRow; }); summaryProjects = summaryProjects.replace("{{.Data}}", projectsSummaryRows); // Edit RunInfo this.runInfo = this.runInfo.replaceAll("{{.Project}}", this.execOptions.project); this.runInfo = this.runInfo.replaceAll("{{.Release}}", this.execOptions.release); this.runInfo = this.runInfo.replaceAll("{{.TestEnvironment}}", this.execOptions.testEnvironment); this.runInfo = this.runInfo.replaceAll("{{.StartTime}}", this.execInfo.startTime); this.runInfo = this.runInfo.replaceAll("{{.EndTime}}", this.execInfo.endTime); this.runInfo = this.runInfo.replaceAll("{{.TotalExecutionTime}}", this.help.convertMsToTime(new Date(this.execInfo.endTime).getTime() - new Date(this.execInfo.startTime).getTime())); // Prepara test data let testCases = ""; let testCasesDetails = ""; let showRetries = false; for (const tc of this.testCases) { if (tc.testCase.retries > 0) { showRetries = true; break; } } this.testCases.forEach((testCase) => { let testCaseId = testCase.testCase.id + "-" + testCase.result.retry; let testCaseHtml = this.testCaseTemplate; testCaseHtml = testCaseHtml.replaceAll("{{.ModalID}}", testCaseId); if (showRetries) { testCaseHtml = testCaseHtml.replaceAll("{{.Retry}}", '<td class="{{.TDClassesCss}}">' + (testCase.result.retry > 0 ? testCase.result.retry.toString() : "") + "</td>"); } else { testCaseHtml = testCaseHtml.replaceAll("{{.Retry}}", ""); } testCaseHtml = testCaseHtml.replaceAll("{{.Title}}", testCase.testCase.title); testCaseHtml = testCaseHtml.replaceAll("{{.Status}}", testCase.result.status); testCaseHtml = testCaseHtml.replaceAll("{{.Duration}}", this.help.convertMsToTime(testCase.result.duration)); let testCaseGroup = testCase.testCase.title !== testCase.testCase.titlePath[3] ? '<td class="{{.TDClassesCss}}">' + testCase.testCase.titlePath[3] + "</td>" : '<td class="{{.TDClassesCss}}"></td>'; testCaseHtml = testCaseHtml.replaceAll("{{.Group}}", this.execOptions.testListColumns.includes("GROUP") ? testCaseGroup : ""); let testCaseProject = '<td class="{{.TDClassesCss}}">' + testCase.testCase.projectId + "</td>"; testCaseHtml = testCaseHtml.replaceAll("{{.Project}}", this.execOptions.testListColumns.includes("PROJECT") ? testCaseProject : ""); let testCaseFile = '<td class="{{.TDClassesCss}}">' + this.help.getShortFilePath(testCase.testCase.filename, this.execOptions.testFolder) + "</td>"; testCaseHtml = testCaseHtml.replaceAll("{{.File}}", this.execOptions.testListColumns.includes("FILE") ? testCaseFile : ""); testCaseHtml = testCaseHtml.replaceAll("{{.TRClassesCss}}", "result-status-" + testCase.result.status); testCaseHtml = testCaseHtml.replaceAll("{{.TDClassesCss}}", "result-status-" + testCase.result.status); testCases += testCaseHtml; let testCaseDetailsHtml = this.testCaseDetailsTemplate; testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.ModalID}}", testCaseId); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.ModalHeaderClassesCss}}", "label-status-" + testCase.result.status); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.ModalHeaderStatus}}", testCase.result.status); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.Title}}", testCase.testCase.title); // Add run info testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.Group}}", testCase.testCase.title !== testCase.testCase.titlePath[3] ? '<tr><th class="odhin-text-2">Group</th><td class="text-secondary-emphasis fst-italic">' + testCase.testCase.titlePath[3] + "</td></tr>" : ""); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoFilename}}", this.help.getShortFilePath(testCase.testCase.filename, this.execOptions.testFolder)); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoProject}}", testCase.testCase.projectId); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoStartTime}}", testCase.result.startTime.toUTCString()); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoTotalExecutionTime}}", this.help.convertMsToTime(testCase.result.duration)); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoRetry}}", showRetries ? '<tr><th class="odhin-text-2">Retry</th><td class="text-secondary-emphasis fst-italic">' + testCase.result.retry.toString() + "</td></tr>" : ""); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoTags}}", testCase.testCase.tags.length > 0 ? '<tr><th class="odhin-text-2">Tags</th><td class="text-secondary-emphasis fst-italic">' + testCase.testCase.tags.join(', ').toString() + "</td></tr>" : ""); if (testCase.testCase.annotations.length > 0) { let annotations = '<tr><th class="odhin-text-2 info-tr-space">Annotations</th><td class="text-secondary-emphasis fst-italic info-tr-space" ></td></tr>'; testCase.testCase.annotations.forEach((annotation) => { annotations += '<tr><th class="odhin-text-2">' + annotation.type + '</th><td class="text-secondary-emphasis fst-italic">' + (this.help.isValidURL(annotation.description) ? '<a target="_Blank" class="link-annotation" href="' + annotation.description + '">' + annotation.description + "</a>" : annotation.description) + "</td></tr>"; }); annotations += "<tr><th></th><td></td></tr>"; testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoAnnotations}}", annotations); } else { testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.InfoAnnotations}}", "<tr><th></th><td></td></tr>"); } // Add errors let errors = ""; let errorFile = ""; if (testCase.result.status === "failed" || testCase.result.status === "timedOut" || testCase.result.status === "interrupted" || testCase.result.status === "flaky") { if (testCase.result.errors !== undefined) { testCase.result.errors.forEach((err) => { if (err.stack !== undefined && err.stack !== "") { errors += err.stack + "\n\n"; let lastLine = err.stack.match(/(.+)([^\s])/g) || ""; errorFile = lastLine[lastLine.length - 1].split(" at ")[1]; } else { errors += err.message + "\n\n"; } }); } testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.ErrorsTabButton}}", this.help.printErrorsTabButton(testCaseId)); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.Errors}}", this.help.printErrors(errors, errorFile !== undefined ? errorFile.length > 1 ? this.help.printErrorCode(errorFile) : "" : "", testCaseId)); } else { testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.ErrorsTabButton}}", ""); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.Errors}}", ""); } // Add steps let steps = ""; testCase.result.steps.forEach((step, index) => { steps += this.help.printStep(testCaseId, index, step, 10); }); testCaseDetailsHtml = testCaseDetailsHtml.replaceAll("{{.Steps}}", `<div class="row m-0 p-0"><div class="col m-0 p-0">` + steps + `</div></div>`); // Add Attachments let screenshots = ""; let screenshotsComparison = ""; let screenshotExpected; let screenshotActual; let screenshotDiff; let videos = ""; let trace = ""; testCase.result.attachments.forEach(async (attachment) => { if (attachment.contentType.startsWith("image/")) { if (attachment.name !== "screenshot") { if (attachment.name.includes("-expected.")) { screenshotExpected = attachment; } else if (attachment.name.includes("-actual.")) { screenshotActual = attachment; } else if (attachment.name.includes("-diff.")) {