UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

467 lines 24 kB
"use strict"; /* * Copyright (c) 2026, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MarkdownTextFormatTransformer = void 0; const node_stream_1 = require("node:stream"); const core_1 = require("@salesforce/core"); const utils_1 = require("../utils"); const markdownTextReporter_1 = require("./markdownTextReporter"); class MarkdownTextFormatTransformer extends node_stream_1.Readable { logger; buffer; bufferSize; testResult; outputFormat; sortOrder; performanceThresholdMs; coverageThresholdPercent; codeCoverage; timestamp; constructor(testResult, options) { super(options); this.testResult = testResult; this.logger = core_1.Logger.childFromRoot('MarkdownTextFormatTransformer'); this.buffer = ''; this.bufferSize = options?.bufferSize || 256; this.outputFormat = options?.format ?? 'markdown'; this.sortOrder = options?.sortOrder ?? 'runtime'; this.performanceThresholdMs = options?.performanceThresholdMs ?? 5000; this.coverageThresholdPercent = options?.coverageThresholdPercent ?? 75; this.codeCoverage = options?.codeCoverage ?? false; this.timestamp = options?.timestamp ?? new Date(); } pushToBuffer(chunk) { this.buffer += chunk; if (this.buffer.length >= this.bufferSize) { this.push(this.buffer); this.buffer = ''; } } _read() { this.logger.trace('starting format'); utils_1.HeapMonitor.getInstance().checkHeapSize('MarkdownTextFormatTransformer._read'); this.format(); if (this.buffer.length > 0) { this.push(this.buffer); } this.push(null); // Signal the end of the stream this.logger.trace('finishing format'); utils_1.HeapMonitor.getInstance().checkHeapSize('MarkdownTextFormatTransformer._read'); } format() { if (this.outputFormat === 'markdown') { this.formatMarkdown(); } else { this.formatText(); } } formatMarkdown() { const reportData = this.buildReportData(); this.renderMarkdown(reportData); } formatText() { const reportData = this.buildReportData(); this.renderText(reportData); } buildReportData() { const { passed, failed, skipped, total, duration } = (0, markdownTextReporter_1.getSummaryInfo)(this.testResult.summary); const timestampStr = (0, markdownTextReporter_1.formatTimestamp)(this.timestamp); // Helper function to sort tests based on sort order const sortTests = (tests) => { if (!tests) { return tests; } return [...tests].sort((a, b) => { const runtimeA = a.runTime ?? 0; const runtimeB = b.runTime ?? 0; const coverageA = this.codeCoverage ? ((0, markdownTextReporter_1.getCoveragePercentage)(a.perClassCoverage?.[0]?.percentage) ?? 100) : 100; const coverageB = this.codeCoverage ? ((0, markdownTextReporter_1.getCoveragePercentage)(b.perClassCoverage?.[0]?.percentage) ?? 100) : 100; if (this.sortOrder === 'runtime') { return runtimeB !== runtimeA ? runtimeB - runtimeA : coverageA - coverageB; } else if (this.sortOrder === 'coverage') { return coverageA !== coverageB ? coverageA - coverageB : runtimeB - runtimeA; } else { const scoreA = (0, markdownTextReporter_1.getSeverityScore)(a, this.codeCoverage, this.performanceThresholdMs, this.coverageThresholdPercent); const scoreB = (0, markdownTextReporter_1.getSeverityScore)(b, this.codeCoverage, this.performanceThresholdMs, this.coverageThresholdPercent); return scoreB - scoreA; } }); }; // Build data structures using spread operators const failedTests = this.testResult.tests?.filter((test) => test.outcome?.toString() === 'Fail') ?? []; const passedTests = this.testResult.tests?.filter((test) => test.outcome?.toString() === 'Pass') ?? []; const skippedTests = this.testResult.tests?.filter((test) => test.outcome?.toString() === 'Skip') ?? []; // Build failures data const failures = failedTests.map((test) => { const { testName } = (0, markdownTextReporter_1.getTestNameInfo)(test); return { testName: (0, markdownTextReporter_1.escapeMarkdown)(testName), ...(test.runTime !== undefined && { duration: (0, markdownTextReporter_1.formatDuration)(test.runTime) }), ...(test.message && { message: test.message }), ...(test.stackTrace && { stackTrace: test.stackTrace }) }; }); // Identify poorly performing and poorly covered tests const poorlyPerformingTests = this.testResult.tests?.filter((test) => (0, markdownTextReporter_1.isPoorlyPerforming)(test.runTime, this.performanceThresholdMs)) ?? []; const poorlyCoveredTests = this.codeCoverage ? (this.testResult.tests?.filter((test) => (0, markdownTextReporter_1.hasPoorCoverage)(test.perClassCoverage?.[0]?.percentage, this.coverageThresholdPercent)) ?? []) : []; // Build warnings data const poorlyPerformingWarnings = [...poorlyPerformingTests] .sort((a, b) => (b.runTime ?? 0) - (a.runTime ?? 0)) .map((test) => { const { testName } = (0, markdownTextReporter_1.getTestNameInfo)(test); return { testName: (0, markdownTextReporter_1.escapeMarkdown)(testName), value: test.runTime !== undefined ? (0, markdownTextReporter_1.formatDuration)(test.runTime) : 'N/A', type: 'performance' }; }); const poorlyCoveredWarnings = [...poorlyCoveredTests] .sort((a, b) => { const coverageA = (0, markdownTextReporter_1.getCoveragePercentage)(a.perClassCoverage?.[0]?.percentage) ?? 0; const coverageB = (0, markdownTextReporter_1.getCoveragePercentage)(b.perClassCoverage?.[0]?.percentage) ?? 0; return coverageA - coverageB; }) .map((test) => { const { testName } = (0, markdownTextReporter_1.getTestNameInfo)(test); const coverage = test.perClassCoverage?.[0]?.percentage ?? 'N/A'; return { testName: (0, markdownTextReporter_1.escapeMarkdown)(testName), value: typeof coverage === 'string' ? coverage : String(coverage), type: 'coverage' }; }); // Build test table data const testTableRows = this.codeCoverage && this.testResult.tests ? sortTests(this.testResult.tests).map((test) => { const { fullClassName, testName } = (0, markdownTextReporter_1.getTestNameInfo)(test); const outcome = test.outcome?.toString() ?? 'Unknown'; const coverage = test.perClassCoverage?.[0]?.percentage ?? 'N/A'; const coverageStr = typeof coverage === 'string' ? coverage : 'N/A'; const runtime = test.runTime !== undefined ? (0, markdownTextReporter_1.formatDuration)(test.runTime) : 'N/A'; const outcomeEmoji = outcome === 'Pass' ? '✅' : outcome === 'Fail' ? '❌' : '⏭️'; const isSlow = (0, markdownTextReporter_1.isPoorlyPerforming)(test.runTime, this.performanceThresholdMs); const hasLowCoverage = (0, markdownTextReporter_1.hasPoorCoverage)(coverage, this.coverageThresholdPercent); return { testName: (0, markdownTextReporter_1.escapeMarkdown)(testName), className: (0, markdownTextReporter_1.escapeMarkdown)(fullClassName), outcome, outcomeEmoji, coverage: coverageStr, runtime, hasWarning: isSlow || hasLowCoverage }; }) : []; // Build passed tests data const passedTestsData = sortTests(passedTests).map((test) => { const { testName } = (0, markdownTextReporter_1.getTestNameInfo)(test); const isSlow = (0, markdownTextReporter_1.isPoorlyPerforming)(test.runTime, this.performanceThresholdMs); const hasLowCoverage = this.codeCoverage && (0, markdownTextReporter_1.hasPoorCoverage)(test.perClassCoverage?.[0]?.percentage, this.coverageThresholdPercent); return { testName: (0, markdownTextReporter_1.escapeMarkdown)(testName), ...(test.runTime !== undefined && { runtime: (0, markdownTextReporter_1.formatDuration)(test.runTime) }), ...(this.codeCoverage && test.perClassCoverage?.[0]?.percentage && { coverage: String(test.perClassCoverage[0].percentage) }), isSlow, hasLowCoverage }; }); // Build skipped tests data const skippedTestsData = skippedTests.map((test) => { const { testName } = (0, markdownTextReporter_1.getTestNameInfo)(test); return { testName: (0, markdownTextReporter_1.escapeMarkdown)(testName) }; }); // Build coverage table data const coverageTableRows = this.codeCoverage && this.testResult.codecoverage ? [...this.testResult.codecoverage] .sort((a, b) => { const percentageA = (0, markdownTextReporter_1.getCoveragePercentage)(a.percentage) ?? 0; const percentageB = (0, markdownTextReporter_1.getCoveragePercentage)(b.percentage) ?? 0; return percentageA - percentageB; }) .map((coverageItem) => { const className = coverageItem.name ?? 'Unknown'; const percentage = coverageItem.percentage ?? '0%'; const uncoveredLines = coverageItem.uncoveredLines ?? []; return { className: (0, markdownTextReporter_1.escapeMarkdown)(className), percentage, uncoveredLines: uncoveredLines.length > 0 ? uncoveredLines.join(', ') : 'None' }; }) : []; // Build report data object const reportData = { timestamp: timestampStr, summary: { total, passed, failed, skipped, duration: (0, markdownTextReporter_1.formatDuration)(duration) }, failures, warnings: { poorlyPerforming: poorlyPerformingWarnings, poorlyCovered: poorlyCoveredWarnings }, ...(testTableRows.length > 0 && { testTable: { rows: testTableRows, note: 'Note: Coverage shown is per-test coverage for the class being tested. Overall class coverage is shown in the "Code Coverage by Class" section below.' } }), passedTests: passedTestsData, skippedTests: skippedTestsData, ...(coverageTableRows.length > 0 && { coverageTable: { rows: coverageTableRows, note: 'This section shows the overall code coverage for each class after all tests have run. This may differ from per-test coverage shown in the table above.' } }) }; return reportData; } renderMarkdown(data) { this.pushToBuffer('# Apex Test Results\n'); this.pushToBuffer(`**Run completed:** ${data.timestamp}\n`); this.pushToBuffer('\n## Summary\n\n'); this.pushToBuffer(`- **Total Tests:** ${data.summary.total}\n`); this.pushToBuffer(`- ✅ **Passed:** ${data.summary.passed}\n`); this.pushToBuffer(`- ❌ **Failed:** ${data.summary.failed}\n`); if (data.summary.skipped > 0) { this.pushToBuffer(`- ⏭️ **Skipped:** ${data.summary.skipped}\n`); } this.pushToBuffer(`- ⏱️ **Duration:** ${data.summary.duration}\n\n`); // Failures section (header uses full-run count from summary) if (data.failures.length > 0) { this.pushToBuffer(`## ❌ Failures (${data.summary.failed})\n\n`); for (const failure of data.failures) { this.pushToBuffer(`### ${failure.testName}\n\n`); if (failure.duration) { this.pushToBuffer(`*Duration: ${failure.duration}*\n\n`); } if (failure.message) { this.pushToBuffer('**Error Message**\n\n'); this.pushToBuffer('```\n'); this.pushToBuffer(`${failure.message}\n`); this.pushToBuffer('```\n\n'); } if (failure.stackTrace) { this.pushToBuffer('**Stack Trace**\n\n'); this.pushToBuffer('```\n'); this.pushToBuffer(`${failure.stackTrace}\n`); this.pushToBuffer('```\n\n'); } this.pushToBuffer('---\n\n'); } } // Warnings section const hasWarnings = data.warnings.poorlyPerforming.length > 0 || data.warnings.poorlyCovered.length > 0; if (hasWarnings) { this.pushToBuffer('## ⚠️ Test Quality Warnings\n\n'); if (data.warnings.poorlyPerforming.length > 0) { this.pushToBuffer(`### 🐌 Poorly Performing Tests (${data.warnings.poorlyPerforming.length})\n\n`); this.pushToBuffer(`*Tests taking longer than ${(0, markdownTextReporter_1.formatDuration)(this.performanceThresholdMs)} (sorted by runtime, slowest first)*\n\n`); for (const test of data.warnings.poorlyPerforming) { this.pushToBuffer(`- **${test.testName}** - **${test.value}**\n`); } this.pushToBuffer('\n'); } if (data.warnings.poorlyCovered.length > 0) { this.pushToBuffer(`### 📉 Poorly Covered Tests (${data.warnings.poorlyCovered.length})\n\n`); this.pushToBuffer(`*Tests with coverage below ${this.coverageThresholdPercent}%*\n\n`); for (const test of data.warnings.poorlyCovered) { this.pushToBuffer(`- **${test.testName}** - ${test.value} coverage\n`); } this.pushToBuffer('\n'); } } // Test results table with coverage if (data.testTable && data.testTable.rows.length > 0) { this.pushToBuffer('## Test Results with Coverage\n\n'); if (data.testTable.note) { this.pushToBuffer(`*${data.testTable.note}*\n\n`); } this.pushToBuffer('<table style="width: 100%; border-collapse: collapse;">\n'); this.pushToBuffer('<thead>\n'); this.pushToBuffer('<tr style="border-bottom: 2px solid;">\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 45%;">Test Name</th>\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 20%;">Class Being Tested</th>\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 12%;">Outcome</th>\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 10%;">Per-Test Coverage</th>\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 13%;">Runtime</th>\n'); this.pushToBuffer('</tr>\n'); this.pushToBuffer('</thead>\n'); this.pushToBuffer('<tbody>\n'); for (const row of data.testTable.rows) { const coverageStyle = row.coverage && row.hasWarning ? 'padding: 8px; font-weight: bold; color: #d32f2f;' : 'padding: 8px;'; const runtimeStyle = row.hasWarning ? 'padding: 8px; font-weight: bold; color: #d32f2f;' : 'padding: 8px;'; this.pushToBuffer('<tr style="border-bottom: 1px solid #ddd;">\n'); this.pushToBuffer(`<td style="padding: 8px;"><code>${row.testName}</code>${row.hasWarning ? ' ⚠️' : ''}</td>\n`); this.pushToBuffer(`<td style="padding: 8px;">${row.className}</td>\n`); this.pushToBuffer(`<td style="padding: 8px;">${row.outcomeEmoji} ${row.outcome}</td>\n`); this.pushToBuffer(`<td style="${coverageStyle}">${row.coverage || 'N/A'}</td>\n`); this.pushToBuffer(`<td style="${runtimeStyle}">${row.runtime}</td>\n`); this.pushToBuffer('</tr>\n'); } this.pushToBuffer('</tbody>\n'); this.pushToBuffer('</table>\n\n'); } // Passed tests section (header uses full-run count from summary) if (data.passedTests.length > 0) { this.pushToBuffer(`## ✅ Passed Tests (${data.summary.passed})\n\n`); for (const test of data.passedTests) { this.pushToBuffer(`- ${test.testName}`); if (test.runtime) { this.pushToBuffer(test.isSlow ? ` (🐌 **${test.runtime}** - slow)` : ` (${test.runtime})`); } if (test.coverage) { this.pushToBuffer(test.hasLowCoverage ? ` (📉 **${test.coverage}** coverage - low)` : ` - ${test.coverage} coverage`); } this.pushToBuffer('\n'); } this.pushToBuffer('\n'); } // Skipped tests section (header uses full-run count from summary) if (data.skippedTests.length > 0) { this.pushToBuffer(`## ⏭️ Skipped Tests (${data.summary.skipped})\n\n`); for (const test of data.skippedTests) { this.pushToBuffer(`- ${test.testName}\n`); } this.pushToBuffer('\n'); } // Code coverage table if (data.coverageTable && data.coverageTable.rows.length > 0) { this.pushToBuffer('## Code Coverage by Class\n\n'); if (data.coverageTable.note) { this.pushToBuffer(`*${data.coverageTable.note}*\n\n`); } this.pushToBuffer('<table style="width: 100%; border-collapse: collapse;">\n'); this.pushToBuffer('<thead>\n'); this.pushToBuffer('<tr style="border-bottom: 2px solid;">\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 30%;">Class</th>\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 15%;">Coverage</th>\n'); this.pushToBuffer('<th style="text-align: left; padding: 8px; width: 55%;">Uncovered Lines</th>\n'); this.pushToBuffer('</tr>\n'); this.pushToBuffer('</thead>\n'); this.pushToBuffer('<tbody>\n'); for (const row of data.coverageTable.rows) { this.pushToBuffer('<tr style="border-bottom: 1px solid #ddd;">\n'); this.pushToBuffer(`<td style="padding: 8px;"><code>${row.className}</code></td>\n`); this.pushToBuffer(`<td style="padding: 8px;">${row.percentage}</td>\n`); this.pushToBuffer(`<td style="padding: 8px;">${row.uncoveredLines}</td>\n`); this.pushToBuffer('</tr>\n'); } this.pushToBuffer('</tbody>\n'); this.pushToBuffer('</table>\n\n'); } } renderText(data) { this.pushToBuffer('Apex Test Results\n'); this.pushToBuffer('==================\n\n'); this.pushToBuffer(`Run completed: ${data.timestamp}\n\n`); // Summary this.pushToBuffer('Summary:\n'); this.pushToBuffer(` Passed: ${data.summary.passed}\n`); this.pushToBuffer(` Failed: ${data.summary.failed}\n`); this.pushToBuffer(` Skipped: ${data.summary.skipped}\n`); this.pushToBuffer(` Total: ${data.summary.total}\n`); this.pushToBuffer(` Duration: ${data.summary.duration}\n\n`); // Failures section if (data.failures.length > 0) { this.pushToBuffer('Failures:\n'); this.pushToBuffer('=========\n\n'); for (const failure of data.failures) { this.pushToBuffer(`${failure.testName}\n`); this.pushToBuffer(`${'-'.repeat(failure.testName.length)}\n`); if (failure.message) { this.pushToBuffer(`Error:\n${failure.message}\n\n`); } if (failure.stackTrace) { this.pushToBuffer(`Stack Trace:\n${failure.stackTrace}\n\n`); } if (failure.duration) { this.pushToBuffer(`Duration: ${failure.duration}\n\n`); } this.pushToBuffer('\n'); } } // Passed tests section if (data.passedTests.length > 0) { this.pushToBuffer('Passed Tests:\n'); this.pushToBuffer('==============\n\n'); for (const test of data.passedTests) { this.pushToBuffer(` - ${test.testName}`); if (test.runtime) { this.pushToBuffer(` (${test.runtime})`); } this.pushToBuffer('\n'); } this.pushToBuffer('\n'); } // Skipped tests section if (data.skippedTests.length > 0) { this.pushToBuffer('Skipped Tests:\n'); this.pushToBuffer('===============\n\n'); for (const test of data.skippedTests) { this.pushToBuffer(` - ${test.testName}\n`); } this.pushToBuffer('\n'); } // Code coverage by class section if (data.coverageTable && data.coverageTable.rows.length > 0) { this.pushToBuffer('Code Coverage by Class:\n'); this.pushToBuffer('=======================\n\n'); for (const row of data.coverageTable.rows) { this.pushToBuffer(` ${row.className}: ${row.percentage} (Uncovered: ${row.uncoveredLines})\n`); } this.pushToBuffer('\n'); } } } exports.MarkdownTextFormatTransformer = MarkdownTextFormatTransformer; __decorate([ (0, utils_1.elapsedTime)() ], MarkdownTextFormatTransformer.prototype, "format", null); //# sourceMappingURL=markdownTextFormatTransformer.js.map