mocha-multiple-sessions-detailed-runner
Version:
High-level API for multi-session Mocha tests with detailed reporting
267 lines (225 loc) • 8.71 kB
text/typescript
import type { IReportService } from '../../interfaces/services/report-service';
import type { DetailedReport, CombinedReport } from '../../interfaces/core/types';
import type { ReportSummary } from '../../interfaces/use-cases/report-use-cases';
import type { IReportRepository } from '../../interfaces/repositories/report-repository';
import type { ISessionMetaRepository } from '../../interfaces/repositories/session-meta-repository';
import type { IUnifiedEventBus } from '../../interfaces/events/unified-event-bus';
export class ReportService implements IReportService {
constructor(
private reportRepository: IReportRepository,
private sessionRepository: ISessionMetaRepository,
private eventBus: IUnifiedEventBus
) {}
getSessionReport(label: string): DetailedReport | null {
const sessionMeta = this.sessionRepository.findByLabel(label);
if (!sessionMeta) return null;
const reportData = this.reportRepository.getReportData(sessionMeta.resultKey);
if (!reportData) return null;
const detailedReport: DetailedReport = {
data: reportData,
stats: reportData.stats || { tests: 0, passes: 0, failures: 0, pending: 0 },
timestamp: reportData.timestamp ? new Date(reportData.timestamp) : new Date(),
sessionLabel: label,
size: JSON.stringify(reportData).length
};
return detailedReport;
}
async waitForSessionReport(label: string, timeoutMs: number = 30000): Promise<DetailedReport> {
const sessionMeta = this.sessionRepository.findByLabel(label);
if (!sessionMeta) {
throw new Error(`Session '${label}' not found`);
}
const reportData = await this.reportRepository.waitForReport(sessionMeta.resultKey, timeoutMs);
return {
data: reportData,
stats: reportData.stats || { tests: 0, passes: 0, failures: 0, pending: 0 },
timestamp: reportData.timestamp ? new Date(reportData.timestamp) : new Date(),
sessionLabel: label,
size: JSON.stringify(reportData).length
};
}
hasSessionReport(label: string): boolean {
const sessionMeta = this.sessionRepository.findByLabel(label);
if (!sessionMeta) return false;
return this.reportRepository.hasReport(sessionMeta.resultKey);
}
generateCombinedReport(): CombinedReport {
const allSessions = this.sessionRepository.findAll();
const sessions: Record<string, DetailedReport> = {};
let totalTests = 0;
let totalPasses = 0;
let totalFailures = 0;
let totalPending = 0;
let successfulSessions = 0;
let failedSessions = 0;
for (const sessionMeta of allSessions) {
const report = this.getSessionReport(sessionMeta.label);
if (report) {
sessions[sessionMeta.label] = report;
totalTests += report.stats.tests;
totalPasses += report.stats.passes;
totalFailures += report.stats.failures;
totalPending += report.stats.pending;
if (report.stats.failures === 0) {
successfulSessions++;
} else {
failedSessions++;
}
}
}
const combinedReport: CombinedReport = {
generatedAt: new Date().toISOString(),
summary: {
totalTests,
totalPasses,
totalFailures,
totalPending,
totalSessions: allSessions.length,
successfulSessions,
failedSessions
},
sessions
};
// Emit report generated event
this.eventBus.emitRunnerEvent({
type: 'runner:combined-report-generated',
timestamp: new Date(),
data: {
reportType: 'combined',
totalSessions: allSessions.length,
reportSize: JSON.stringify(combinedReport).length,
generatedAt: new Date(),
summary: combinedReport.summary
}
});
return combinedReport;
}
async getCombinedReportAsync(): Promise<CombinedReport> {
// For now, just return the sync version
// In the future, this could wait for all reports to be ready
return this.generateCombinedReport();
}
watchSessionReport(label: string, callback: (report: DetailedReport) => void): () => void {
const sessionMeta = this.sessionRepository.findByLabel(label);
if (!sessionMeta) {
throw new Error(`Session '${label}' not found`);
}
return this.reportRepository.watchReport(sessionMeta.resultKey, (reportData: any) => {
const detailedReport: DetailedReport = {
data: reportData,
stats: reportData.stats || { tests: 0, passes: 0, failures: 0, pending: 0 },
timestamp: reportData.timestamp ? new Date(reportData.timestamp) : new Date(),
sessionLabel: label,
size: JSON.stringify(reportData).length
};
callback(detailedReport);
});
}
watchAllReports(callback: (sessionLabel: string, report: DetailedReport) => void): () => void {
const unsubscribeFunctions: (() => void)[] = [];
const allSessions = this.sessionRepository.findAll();
for (const sessionMeta of allSessions) {
const unsubscribe = this.watchSessionReport(sessionMeta.label, (report) => {
callback(sessionMeta.label, report);
});
unsubscribeFunctions.push(unsubscribe);
}
// Return function to unsubscribe from all
return () => {
unsubscribeFunctions.forEach(fn => fn());
};
}
getReportSummary(): ReportSummary {
const allSessions = this.sessionRepository.findAll();
const reportSizes: Record<string, number> = {};
let totalReports = 0;
let totalTests = 0;
let totalPasses = 0;
let totalFailures = 0;
let oldestReport: Date | undefined;
let newestReport: Date | undefined;
for (const sessionMeta of allSessions) {
const report = this.getSessionReport(sessionMeta.label);
if (report) {
totalReports++;
totalTests += report.stats.tests;
totalPasses += report.stats.passes;
totalFailures += report.stats.failures;
reportSizes[sessionMeta.label] = report.size || 0;
if (!oldestReport || report.timestamp < oldestReport) {
oldestReport = report.timestamp;
}
if (!newestReport || report.timestamp > newestReport) {
newestReport = report.timestamp;
}
}
}
return {
totalReports,
totalTests,
totalPasses,
totalFailures,
reportSizes,
...(oldestReport && { oldestReport }),
...(newestReport && { newestReport })
};
}
async exportReports(format: 'json' | 'html'): Promise<string> {
const combinedReport = this.generateCombinedReport();
if (format === 'json') {
return JSON.stringify(combinedReport, null, 2);
} else {
// Basic HTML export - could be enhanced
return `
<html>
<head><title>Test Report</title></head>
<body>
<h1>Combined Test Report</h1>
<p>Generated: ${combinedReport.generatedAt}</p>
<h2>Summary</h2>
<pre>${JSON.stringify(combinedReport.summary, null, 2)}</pre>
<h2>Sessions</h2>
<pre>${JSON.stringify(combinedReport.sessions, null, 2)}</pre>
</body>
</html>
`;
}
}
clearAllReports(): void {
const allSessions = this.sessionRepository.findAll();
for (const sessionMeta of allSessions) {
if (this.reportRepository.hasReport(sessionMeta.resultKey)) {
this.reportRepository.clearReport(sessionMeta.resultKey);
}
}
}
validateSessionReport(sessionLabel: string): boolean {
const report = this.getSessionReport(sessionLabel);
return report !== null && this.reportRepository.validateReport(report.data);
}
getReportValidationErrors(sessionLabel: string): string[] {
const errors: string[] = [];
const report = this.getSessionReport(sessionLabel);
if (!report) {
errors.push('Report not found');
return errors;
}
if (!report.data) {
errors.push('Report data is missing');
}
if (!report.stats) {
errors.push('Report stats are missing');
} else {
if (typeof report.stats.tests !== 'number') {
errors.push('Invalid test count');
}
if (typeof report.stats.passes !== 'number') {
errors.push('Invalid pass count');
}
if (typeof report.stats.failures !== 'number') {
errors.push('Invalid failure count');
}
}
return errors;
}
}