UNPKG

@uuv/playwright

Version:

A solution to facilitate the writing and execution of E2E tests understandable by any human being using cucumber(BDD) and playwright

586 lines (585 loc) 25.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeneratedReportType = void 0; const gherkin_1 = require("@cucumber/gherkin"); const gherkin_utils_1 = require("@cucumber/gherkin-utils"); const messages_1 = require("@cucumber/messages"); const fs_1 = __importDefault(require("fs")); const multiple_cucumber_html_reporter_1 = __importDefault(require("multiple-cucumber-html-reporter")); const nanoid_1 = require("nanoid"); const chalk_1 = __importDefault(require("chalk")); const chalk_table_1 = __importDefault(require("chalk-table")); const uuv_custom_formatter_1 = require("./uuv-custom-formatter"); const tag_expressions_1 = __importDefault(require("@cucumber/tag-expressions")); const path_1 = __importDefault(require("path")); const event_1 = require("@uuv/runner-commons/runner/event"); const utils_1 = require("@uuv/runner-commons/runner/utils"); const NANOS_IN_SECOND = 1000000000; const NANOS_IN_MILLISSECOND = 1000000; var GeneratedReportType; (function (GeneratedReportType) { GeneratedReportType["CONSOLE"] = "console"; GeneratedReportType["HTML"] = "html"; })(GeneratedReportType || (exports.GeneratedReportType = GeneratedReportType = {})); class ReportOfFeature { passed = 0; skipped = 0; failed = 0; retries = 0; increment(status, retry) { if (retry > 0) { this.retries++; } if (status === "passed") { this.passed++; } else if (status === "skipped") { this.skipped++; } else if (status === "failed") { this.failed++; } } } class UuvPlaywrightReporterHelper { testDir; queries = new Map(); envelopes = []; featureFileAndTestCaseStatusMap = new Map(); testCasesAndTestCasesStartedIdMap = new Map(); testCasesAndPickleIdMap = new Map(); testCasesTestStepStartedIdMap = new Map(); testStepLocationAndTestStepIdMap = new Map(); consoleReportMap = new Map(); errors = []; currentFeatureFile; foundConf; constructor() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const bddgenConfAsString = process.env.PLAYWRIGHT_BDD_CONFIGS; if (bddgenConfAsString) { const bddgenConf = JSON.parse(bddgenConfAsString); if (Object.keys.length > 0) { const foundPath = Object.keys(bddgenConf)[0]; this.foundConf = bddgenConf[foundPath]; } } } createTestRunStartedEnvelope(config, suite, startTimestamp) { this.testDir = config.projects[0].testDir; const featureFiles = this.getFeatureFiles(suite); this.initializeCucumberReportNdJson(suite, featureFiles); this.envelopes.push(this.createEnvelope({ testRunStarted: { timestamp: startTimestamp } })); } createTestCaseStartedEnvelope(test, result, featureFile, startTimestamp) { const currentQuery = this.queries.get(featureFile); if (currentQuery) { const newTestCaseEnvelope = this.createEnvelope({ testCase: { id: test.id, pickleId: this.testCasesAndPickleIdMap.get(test.id), testSteps: this.generateTestStep(currentQuery, test) } }); let newCurrentQuery = currentQuery.update(newTestCaseEnvelope); this.envelopes.push(newTestCaseEnvelope); const testCaseStartedId = (0, nanoid_1.nanoid)(); const newTestCaseStartedEnvelope = this.createEnvelope({ testCaseStarted: { id: testCaseStartedId, testCaseId: test.id, attempt: result.retry, timestamp: startTimestamp } }); newCurrentQuery = newCurrentQuery.update(newTestCaseStartedEnvelope); this.envelopes.push(newTestCaseStartedEnvelope); this.queries.set(featureFile, newCurrentQuery); this.testCasesAndTestCasesStartedIdMap.set(test.id, testCaseStartedId); this.initConsoleReportIfNotExists(featureFile); } event_1.UUVEventEmitter.getInstance().emitTestStarted({ testName: test.title, testSuiteName: this.getTestSuiteName(featureFile), testSuitelocation: featureFile }); } createTestStepStartedEnvelope(test, step, featureFile, startTimestamp) { const currentQuery = this.queries.get(featureFile); if (currentQuery && step.location) { let newIndexStep = this.testCasesTestStepStartedIdMap.get(test.id); newIndexStep = newIndexStep ? newIndexStep + 1 : 1; const pickleId = this.testCasesAndPickleIdMap.get(test.id); const pickle = currentQuery.getPickles() .find(pickle => pickle.id === pickleId); const testStep = pickle?.steps[newIndexStep - 1]; if (testStep?.id && step.title.endsWith(testStep.text)) { this.testCasesTestStepStartedIdMap.set(test.id, newIndexStep); this.testStepLocationAndTestStepIdMap.set(this.getTestStepKey(step.location), testStep.id); const newTestStepEnvelope = this.createEnvelope({ testStepStarted: { testStepId: testStep.id, testCaseStartedId: this.testCasesAndTestCasesStartedIdMap.get(test.id), timestamp: startTimestamp } }); this.envelopes.push(newTestStepEnvelope); } } } createTestStepSkippedEnvelope(test, featureFile, timestamp) { const currentQuery = this.queries.get(featureFile); if (currentQuery) { const newIndexStepTemp = this.testCasesTestStepStartedIdMap.get(test.id); const newIndexStep = newIndexStepTemp !== undefined ? newIndexStepTemp + 1 : 1; if (newIndexStep !== undefined) { const pickleId = this.testCasesAndPickleIdMap.get(test.id); currentQuery.getPickles() .find(pickle => pickle.id === pickleId) ?.steps .filter((value, index) => index >= (newIndexStep - 1)) .forEach((pickleStep) => { const newTestStepStartedEnvelope = this.createEnvelope({ testStepStarted: { testStepId: pickleStep.id, testCaseStartedId: this.testCasesAndTestCasesStartedIdMap.get(test.id), timestamp: timestamp } }); this.envelopes.push(newTestStepStartedEnvelope); const newTestStepSkippedEnvelope = this.createEnvelope({ testStepFinished: { testStepId: this.testCasesAndTestCasesStartedIdMap.get(test.id), testCaseStartedId: pickleStep.id, testStepResult: { status: "SKIPPED", duration: { seconds: 0, nanos: 0 } }, timestamp: timestamp } }); this.envelopes.push(newTestStepSkippedEnvelope); }); } } } createTestStepFinishedEnvelope(test, step, result, featureFile, startTimestamp) { const currentQuery = this.queries.get(featureFile); if (currentQuery && step.location) { const testStepId = this.testStepLocationAndTestStepIdMap.get(this.getTestStepKey(step.location)); if (testStepId) { const newTestStepEnvelope = this.createEnvelope({ testStepFinished: { testStepId: testStepId, testCaseStartedId: this.testCasesAndTestCasesStartedIdMap.get(test.id), testStepResult: { status: this.getStatus(step), duration: { seconds: result.duration / 1000, nanos: result.duration * NANOS_IN_MILLISSECOND }, // eslint-disable-next-line no-control-regex message: step.error?.message?.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "") }, timestamp: startTimestamp } }); this.envelopes.push(newTestStepEnvelope); } } } getTestStepKey(location) { return `${location.file.replaceAll("\\", "_")}-${location.line}-${location.column}`; } createTestCaseFinishedEnvelope(test, result, featureFile, endTimestamp) { this.handleTestEnd(result, test, featureFile); this.updateTestcaseStatus(featureFile, test.id, "done"); const currentQuery = this.queries.get(featureFile); if (currentQuery) { if (result.status === "skipped" || result.status === "failed") { this.createTestStepSkippedEnvelope(test, featureFile, endTimestamp); } const testCaseStartedId = this.testCasesAndTestCasesStartedIdMap.get(test.id); const newTestCaseFinishedEnvelope = this.createEnvelope({ testCaseFinished: { testCaseStartedId: testCaseStartedId, attempt: result.retry, timestamp: endTimestamp } }); currentQuery.update(newTestCaseFinishedEnvelope); this.envelopes.push(newTestCaseFinishedEnvelope); this.updateConsoleReport(featureFile, result); this.addResultErrors(result, test, featureFile); this.createTestCaseErrorAttachmentsEnvelope(testCaseStartedId, result); } this.handleTestSuiteFinishedIfNeeded(featureFile); } handleTestEnd(result, test, featureFile) { switch (result.status) { case "passed": event_1.UUVEventEmitter.getInstance().emitTestFinished({ testName: test.title, testSuiteName: this.getTestSuiteName(featureFile), duration: result.duration }); break; case "failed": event_1.UUVEventEmitter.getInstance().emitTestFailed({ testName: test.title, testSuiteName: this.getTestSuiteName(featureFile), duration: result.duration }); break; default: event_1.UUVEventEmitter.getInstance().emitTestIgnored({ testName: test.title, testSuiteName: this.getTestSuiteName(featureFile) }); } } handleTestSuiteFinishedIfNeeded(featureFile) { const featureTestCaseStatus = this.featureFileAndTestCaseStatusMap.get(featureFile); if (featureTestCaseStatus) { if (Object.entries(featureTestCaseStatus).find(([, value]) => value === "todo") === undefined) { event_1.UUVEventEmitter.getInstance().emitTestSuiteFinished({ testSuiteName: this.getTestSuiteName(featureFile) }); } } } addResultErrors(result, test, featureFile) { result.errors.forEach(error => { const scenario = this.getCurrentRunningScenario(test, featureFile); this.addError({ scenario: scenario, error: error.message }); }); } createTestCaseErrorAttachmentsEnvelope(testCaseStartedId, result) { result.attachments.forEach(attachment => { if (attachment.path && attachment.path.endsWith("test-failed-1.png")) { const attachmentBody = fs_1.default.readFileSync(attachment.path, { encoding: "base64" }); const failedStep = result.steps.find(step => step.error); if (failedStep?.location) { const testCaseAttachmentEnvelope = this.createEnvelope({ attachment: { testCaseStartedId: testCaseStartedId, testStepId: this.testStepLocationAndTestStepIdMap.get(this.getTestStepKey(failedStep.location)), body: attachmentBody, contentEncoding: messages_1.AttachmentContentEncoding.BASE64, mediaType: attachment.contentType } }); this.envelopes.push(testCaseAttachmentEnvelope); } } }); } initConsoleReportIfNotExists(featureFile) { if (!this.consoleReportMap.get(featureFile)) { event_1.UUVEventEmitter.getInstance().emitTestSuiteStarted({ testSuiteName: this.getTestSuiteName(featureFile), testSuitelocation: featureFile }); this.consoleReportMap.set(featureFile, new ReportOfFeature()); } } updateConsoleReport(featureFile, result) { this.consoleReportMap.get(featureFile)?.increment(result.status, result.retry); } createTestRunFinishedEnvelope(result) { const endDate = new Date(); const durationInSecond = endDate.getTime() / 1000; this.envelopes.push((0, messages_1.parseEnvelope)(JSON.stringify({ testRunFinished: { success: result.status === "passed", timestamp: { seconds: durationInSecond, nanos: Math.floor(durationInSecond * NANOS_IN_SECOND + durationInSecond) } } }))); } async generateReport(reportType) { this.displayConsoleReport(); if (reportType === GeneratedReportType.HTML) { await this.generateHtmlReport(); } } getTimestamp(inpuDate) { const date = inpuDate ? inpuDate : new Date(); const durationInSecond = date.getTime() / 1000; const NANOS_IN_SECOND = 1000000000; return { seconds: durationInSecond, nanos: Math.floor(durationInSecond * NANOS_IN_SECOND + durationInSecond) }; } getOriginalFeatureFile(generatedFile) { if (this.foundConf) { return path_1.default.relative(process.cwd(), generatedFile .replaceAll(this.foundConf.outputDir, this.foundConf.featuresRoot) .replaceAll(".feature.spec.js", ".feature")); } return; } getTestSuiteName(featureFile) { if (this.foundConf) { return path_1.default.relative(this.foundConf.featuresRoot, path_1.default.join(process.cwd(), featureFile)); } return featureFile; } getCurrentRunningScenario(test, featureFile) { const counter = `[${test.parent.allTests().filter(test => test.results.length > 0).length}/${test.parent.allTests().length}]`; return `File ${featureFile} > Scenario ${counter} - ${test.title}`; } addError(newError) { this.errors.push(newError); } getStatus(step) { if (step.error === undefined) { return "PASSED"; } else { return "FAILED"; } } createEnvelope(content) { return (0, messages_1.parseEnvelope)(JSON.stringify(content)); } createCucumberNdJsonFile(outputFileName) { try { console.log(`writting file ${outputFileName}`); this.envelopes.forEach((envelope, index) => { fs_1.default.writeFileSync(outputFileName, `${JSON.stringify(envelope)}\r\n`, { flag: index === 0 ? "w+" : "a" }); }); } catch (err) { console.log(err); } } initializeCucumberReportNdJson(suite, featureFiles) { featureFiles.forEach(featureFile => { const originalFile = this.getOriginalFeatureFile(featureFile); const tagsParameter = this.getTagsParameter(); const tagsExpression = (0, tag_expressions_1.default)(tagsParameter ? tagsParameter : ""); if (originalFile) { const currentEnvelopes = (0, gherkin_1.generateMessages)(fs_1.default.readFileSync(originalFile).toString(), originalFile, messages_1.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, { includeSource: true, includeGherkinDocument: true, newId: messages_1.IdGenerator.uuid(), includePickles: true }); const currentQuery = new gherkin_utils_1.Query(); currentEnvelopes .map(envelope => { if (tagsParameter && envelope?.gherkinDocument?.feature) { envelope.gherkinDocument.feature.children = envelope.gherkinDocument.feature.children.filter(child => child.scenario?.tags && tagsExpression.evaluate(child.scenario.tags.map(tag => tag.name))); } return envelope; }) .filter(envelope => { if (tagsParameter && envelope?.pickle?.steps) { return tagsExpression.evaluate(envelope.pickle.tags.map(pickleTag => pickleTag.name)); } return true; }) .forEach(envelope => currentQuery.update(envelope)); this.queries.set(originalFile, currentQuery); this.envelopes = this.envelopes.concat(currentEnvelopes); this.featureFileAndTestCaseStatusMap.set(originalFile, []); this.populateTestCasesAndPickleIdMap(currentQuery, suite, featureFile, originalFile); } }); } populateTestCasesAndPickleIdMap(currentQuery, suite, featureFile, originalFile) { const pickles = currentQuery.getPickles(); const featureFileTestSuite = suite.allTests() .filter(testCase => testCase.location.file === featureFile); featureFileTestSuite.forEach((testCase, index) => { this.testCasesAndPickleIdMap.set(testCase.id, pickles[index].id); this.updateTestcaseStatus(originalFile, testCase.id, "todo"); }); } updateTestcaseStatus(originalFile, testCaseId, newStatus) { const featureTestCaseStatus = this.featureFileAndTestCaseStatusMap.get(originalFile); if (featureTestCaseStatus) { featureTestCaseStatus[`${testCaseId}`] = newStatus; this.featureFileAndTestCaseStatusMap.set(originalFile, featureTestCaseStatus); } } generateTestStep(currentQuery, test) { const pickleId = this.testCasesAndPickleIdMap.get(test.id); return currentQuery.getPickles() .find(pickle => pickle.id === pickleId) ?.steps .map((step) => { return { id: step.id, pickleStepId: step.id, stepDefinitionIds: [this.createStepDefinitionEnvelope()] }; }); } createStepDefinitionEnvelope() { const stepDefinitionId = (0, nanoid_1.nanoid)(); this.envelopes.push(this.createEnvelope({ stepDefinition: { id: stepDefinitionId, pattern: { source: "a step", type: "CUCUMBER_EXPRESSION" }, sourceReference: { uri: "not available", location: { line: 0 } } } })); return stepDefinitionId; } getFeatureFiles(suite) { return [...new Set(suite.allTests().map(testCase => testCase.location?.file))]; } async formatCucumberMessageFile(inputMessageFile, outputFormattedFileJson) { const formatter = new uuv_custom_formatter_1.UuvCustomFormatter(); await formatter.parseCucumberJson(inputMessageFile, outputFormattedFileJson); } generateHtmlReportFromJson(reportDirHtml, reportDirJson) { const UNKNOWN_VALUE = "unknown"; multiple_cucumber_html_reporter_1.default.generate({ jsonDir: reportDirJson, reportPath: reportDirHtml, useCDN: true, metadata: { browser: { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore name: process.env.browser, version: UNKNOWN_VALUE }, device: UNKNOWN_VALUE, platform: { name: UNKNOWN_VALUE, version: UNKNOWN_VALUE, }, }, }); (0, utils_1.updateChartJsVersion)(reportDirHtml, "2.5.0", "2.6.0"); } async generateHtmlReport() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const reportDir = `${process.env.CONFIG_DIR}/reports/e2e`; const outputMessageFile = `${reportDir}/cucumber-messages.ndjson`; const reportDirHtml = `${reportDir}/html`; const reportDirJson = `${reportDir}/json`; const outputFormattedFileJson = `${reportDirJson}/cucumber-report.json`; this.createdDirIfNeeded(reportDir); this.createdDirIfNeeded(reportDirHtml); this.createdDirIfNeeded(reportDirJson); this.createCucumberNdJsonFile(outputMessageFile); await this.formatCucumberMessageFile(outputMessageFile, outputFormattedFileJson); this.generateHtmlReportFromJson(reportDirHtml, reportDirJson); } createdDirIfNeeded(reportDir) { // Creating needed dirs if (!fs_1.default.existsSync(reportDir)) { fs_1.default.mkdirSync(reportDir, { recursive: true }); } } displayConsoleReport() { const consoleReport = []; const chalkTableOptions = { leftPad: 2, columns: [ { field: "id", name: "Id" }, { field: "file", name: "File" }, { field: "passed", name: chalk_1.default.green("Passed") }, { field: "skipped", name: chalk_1.default.yellow("Skipped") }, { field: "failed", name: chalk_1.default.redBright("Failed") }, { field: "retries", name: chalk_1.default.yellow("retries") } ] }; let index = 1; this.consoleReportMap.forEach((value, key) => { consoleReport.push({ id: index, file: key, passed: chalk_1.default.green(value.passed), retries: chalk_1.default.yellow(value.retries), skipped: chalk_1.default.yellow(value.skipped), failed: value.failed ? chalk_1.default.redBright(value.failed) : chalk_1.default.red(value.failed) }); index++; }); if (this.errors.length > 0) { console.log("\n\n"); console.log(chalk_1.default.underline(chalk_1.default.bgRed(`${this.errors.length} Error(s) :`))); this.errors.forEach(error => { console.log(error.scenario); console.log(chalk_1.default.red(error.error.toString())); console.log(""); }); console.log(""); } console.log(chalk_1.default.underline(chalk_1.default.bgBlue("Test Execution Report :"))); console.log((0, chalk_table_1.default)(chalkTableOptions, consoleReport)); console.log("\n\n"); } logTestBegin(testCase, featureFile) { if (this.currentFeatureFile !== featureFile) { console.info(chalk_1.default.blueBright(` Feature: ${featureFile}`)); this.currentFeatureFile = featureFile; } } logTestEnd(testCase, result) { console.info(` ${this.getResultIcon(testCase)} ${this.getTestCaseTitle(testCase, result)}`); } getResultIcon(testCase) { if (testCase.expectedStatus === "skipped") { return chalk_1.default.yellowBright("\u2192"); } else if (testCase.ok()) { return chalk_1.default.green("\u2713"); } else { return chalk_1.default.redBright("\u166D"); } } getTestCaseTitle(testCase, result) { const message = `${testCase.title} (${result.duration}ms)`; if (testCase.expectedStatus === "skipped") { return chalk_1.default.yellowBright(message); } else if (testCase.ok()) { return chalk_1.default.gray(message); } else { return chalk_1.default.redBright(message); } } getTagsParameter() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return process.env.TAGS; } } exports.default = UuvPlaywrightReporterHelper;