jest-markdown-expand-reporter
Version:
A markdown reporter for Jest tests with expandable sections containing per test console logs
171 lines (170 loc) • 7.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
let fs = null;
let path = null;
let os = null;
let MDSummaryGenerator = null;
let MDTestByTestGenerator = null;
try {
fs = require('fs');
}
catch { }
try {
path = require('path');
}
catch { }
try {
os = require('os');
}
catch { }
try {
MDSummaryGenerator = require('./mdSummaryGenerator').default || require('./mdSummaryGenerator');
MDTestByTestGenerator =
require('./mdTestByTestGenerator').default || require('./mdTestByTestGenerator');
}
catch { }
const fileLockUtils_1 = require("./fileLockUtils");
var LogOptions;
(function (LogOptions) {
// Display console logs in the report
LogOptions["ALL_LOGS"] = "all";
LogOptions["DEBUG"] = "debug";
LogOptions["ERROR"] = "error";
LogOptions["INFO"] = "info";
LogOptions["LOG"] = "log";
LogOptions["WARN"] = "warn";
})(LogOptions || (LogOptions = {}));
class MDReporter {
constructor(globalConfig, reporterOptions) {
this.logs = {};
this.globalConfig = globalConfig;
this.options = reporterOptions ?? {};
this.filename = this.options.filename ?? 'test-results.md';
this.publicPath = this.options.publicPath ?? './';
this.ciOutput = this.options.ciOutput ?? [];
this.consoleLogs = this.options.consoleLogs ?? [];
this.displayAllTests = this.options.displayAllTests ?? false;
this.failureMessages = this.options.failureMessages ?? true;
this.prioritizeFailures = this.options.prioritizeFailures ?? false;
this.skipDisplayIfNoFailures = this.options.skipDisplayIfNoFailures ?? true;
this.enableAnnotations =
this.options.enableAnnotations ?? 'GITHUB_STEP_SUMMARY' in this.ciOutput;
this.startTime = new Date();
this.logs = {};
}
getOptions() {
return {
consoleLogs: this.consoleLogs,
displayAllTests: this.displayAllTests,
failureMessages: this.failureMessages,
prioritizeFailures: this.prioritizeFailures,
skipDisplayIfNoFailures: this.skipDisplayIfNoFailures,
};
}
async onTestResult(test, testResult) {
if (!path)
return;
const suitePath = test.path ?? test.testFilePath ?? 'unknown-suite';
const suiteName = path.basename(suitePath, path.extname(suitePath));
const testKey = `${suiteName}::${test.fullName ?? test.title}`;
this.logs[testKey] = testResult.console ?? [];
}
async onRunComplete(contexts, runResults) {
if (!fs || !path || !os || !MDSummaryGenerator)
return false;
const tmpDir = os.tmpdir();
const enabledLogTypes = (this.consoleLogs ?? []).map((type) => type.toLowerCase());
// For each test suite, read its corresponding log file
for (const suite of runResults.testResults) {
const suiteFile = suite.testFilePath
? path.basename(suite.testFilePath, path.extname(suite.testFilePath))
: 'unknown-suite';
const logFile = path.join(tmpDir, `jest-md-logs-${suiteFile}.json`);
let suiteLogs = {};
if (fs.existsSync(logFile)) {
try {
suiteLogs = JSON.parse(fs.readFileSync(logFile, 'utf-8'));
}
catch (e) {
process.stderr.write(`Could not parse log file ${logFile}: ${e}\n`);
}
}
// Attach logs to each test in the suite, filtering by enabled log types
for (const test of suite.testResults) {
const testName = test.fullName ?? test.title;
let logs = suiteLogs[testName] ?? [];
// Only include logs if enabled in options
if (enabledLogTypes.includes('all') ||
enabledLogTypes.some((type) => logs.some((log) => (log.type ?? '').toLowerCase() === type))) {
// If not 'all', filter logs by enabled types
if (!enabledLogTypes.includes('all')) {
logs = logs.filter((log) => enabledLogTypes.some((type) => (log.type ?? '').toLowerCase() === type));
}
test.consoleLogs = logs;
}
else {
test.consoleLogs = [];
}
// Output GitHub Actions annotations for each failed test if enabled
if (this.enableAnnotations && process.env.GITHUB_STEP_SUMMARY) {
if (test.status === 'failed' && test.failureMessages && test.failureMessages.length > 0) {
// Try to extract line/column from failure stack, fallback to suiteFile
let file = suiteFile;
let line = 1;
let col = 1;
const stack = test.failureMessages.join('\n');
const match = stack.match(/at .*\((.*):(\d+):(\d+)\)/) || stack.match(/at (.*):(\d+):(\d+)/);
if (match) {
file = match[1] || suiteFile;
line = parseInt(match[2] || '1', 10);
col = parseInt(match[3] || '1', 10);
}
// Output GitHub Actions error annotation
process.stdout.write(`::error file=${file},line=${line},col=${col}::${test.fullName || test.title}\n`);
}
}
}
}
// Prepare data for the EJS template
const data = {
packageName: this.globalConfig.rootDir ? path.basename(this.globalConfig.rootDir) : 'Project',
date: this.startTime,
testResults: runResults.testResults,
...this.getOptions(),
// Add these summary fields:
numPassedTestSuites: runResults.numPassedTestSuites,
numFailedTestSuites: runResults.numFailedTestSuites,
numPendingTestSuites: runResults.numPendingTestSuites,
numTotalTestSuites: runResults.numTotalTestSuites,
numPassedTests: runResults.numPassedTests,
numFailedTests: runResults.numFailedTests,
numPendingTests: runResults.numPendingTests,
numTotalTests: runResults.numTotalTests,
};
try {
const summaryReport = await MDSummaryGenerator.generate(data, data.date);
const testByTestReport = await MDTestByTestGenerator.generate(data, data.date);
if (!fs.existsSync(this.publicPath))
fs.mkdirSync(this.publicPath, { recursive: true });
const filename = path.join(this.publicPath, this.filename);
fs.writeFileSync(filename, summaryReport + testByTestReport, 'utf8');
// If CI output is specified, write the report to each specified environment variable
if (this.ciOutput.length > 0) {
for (const envVar of this.ciOutput) {
const envPath = process.env[envVar];
if (envPath) {
await (0, fileLockUtils_1.safePrependAppend)(envPath, summaryReport, testByTestReport, 'utf8');
}
else {
process.stderr.write(`Environment variable ${envVar} is not set.\n`);
}
}
}
}
catch (err) {
process.stderr.write(`Markdown report generation failed: ${JSON.stringify(err, null, 2)}\n`);
process.exitCode = 1;
}
}
}
exports.default = MDReporter;