js-tests-results-collector
Version:
Universal test results collector for Jest, Jasmine, Mocha, Cypress, and Playwright that sends results to Buddy Works API
306 lines (259 loc) • 10 kB
JavaScript
const sessionManager = require('../../core/session-manager');
const TestResultMapper = require('../../utils/test-result-mapper');
const Logger = require('../../utils/logger');
class VitestReporter {
constructor() {
this.logger = new Logger('VitestReporter');
this.setupProcessExitHandlers();
this.tasks = new Map(); // Store tasks to resolve names from IDs
this.ctx = null; // Store context for later use
this.processedTests = new Set(); // Track processed tests to avoid duplicates
}
setupProcessExitHandlers() {
// Handle process exit - use beforeExit for async operations
process.on('beforeExit', async () => {
this.logger.debug('Process about to exit, closing session');
try {
await sessionManager.closeSession();
this.logger.debug('Session closed successfully');
} catch (error) {
this.logger.error('Error closing session on beforeExit', error);
// Mark this as a framework error
sessionManager.markFrameworkError();
}
});
// Handle SIGINT (Ctrl+C)
process.on('SIGINT', async () => {
this.logger.debug('Received SIGINT, closing session');
try {
await sessionManager.closeSession();
this.logger.debug('Session closed successfully on SIGINT');
} catch (error) {
this.logger.error('Error closing session on SIGINT', error);
// Mark this as a framework error
sessionManager.markFrameworkError();
}
process.exit(0);
});
// Handle SIGTERM
process.on('SIGTERM', async () => {
this.logger.debug('Received SIGTERM, closing session');
try {
await sessionManager.closeSession();
this.logger.debug('Session closed successfully on SIGTERM');
} catch (error) {
this.logger.error('Error closing session on SIGTERM', error);
// Mark this as a framework error
sessionManager.markFrameworkError();
}
process.exit(0);
});
}
// Override onInit to capture context
onInit(ctx) {
this.logger.debug('Vitest reporter initialized');
this.ctx = ctx; // Store context for later use
// Log available context properties for debugging
this.logger.debug('Context properties:', Object.keys(ctx || {}));
if (ctx && ctx.state) {
this.logger.debug('Context state properties:', Object.keys(ctx.state || {}));
this.loadTasksFromContext();
}
this.logger.debug(`Total tasks stored: ${this.tasks.size}`);
}
// Simplified method to load tasks from context
loadTasksFromContext() {
if (!this.ctx || !this.ctx.state) return;
try {
// Method 1: getFiles() - most common approach
if (this.ctx.state.getFiles) {
const files = this.ctx.state.getFiles();
this.logger.debug(`getFiles(): Found ${files ? files.length : 0} files`);
if (files && files.length > 0) {
files.forEach(file => this.traverseTasks(file));
}
}
// Method 2: Direct files array
if (this.ctx.state.files && Array.isArray(this.ctx.state.files)) {
this.logger.debug(`Direct files: Found ${this.ctx.state.files.length} files`);
this.ctx.state.files.forEach(file => this.traverseTasks(file));
}
// Method 3: Task maps - fallback for direct task access
const taskMaps = ['idMap', 'taskMap', 'filesMap'];
taskMaps.forEach(mapName => {
if (this.ctx.state[mapName] instanceof Map) {
this.logger.debug(`${mapName}: Found ${this.ctx.state[mapName].size} entries`);
this.ctx.state[mapName].forEach((value, key) => {
if (value && (value.name || value.id)) {
this.tasks.set(key, value);
}
});
}
});
} catch (error) {
this.logger.error('Error loading tasks from context', error);
}
}
// Recursively traverse tasks to build ID->name mapping
traverseTasks(task) {
if (task && task.id) {
this.tasks.set(task.id, task);
this.logger.debug(`Stored task: ${task.id} -> ${task.name || task.file || 'unknown'}`);
}
if (task && task.tasks && Array.isArray(task.tasks)) {
task.tasks.forEach(subTask => this.traverseTasks(subTask));
}
}
async onTaskUpdate(packs) {
try {
// Try to load tasks from context if we haven't loaded them yet
if (this.tasks.size === 0) {
this.logger.debug('Attempting to load tasks from context in onTaskUpdate');
this.loadTasksFromContext();
}
for (const pack of packs) {
if (pack[1]) {
await this.processTaskUpdate(pack[0], pack[1]);
}
}
} catch (error) {
this.logger.error('Error processing task update', error);
}
}
async processTaskUpdate(taskId, taskResult) {
// Determine if this task should be processed
const shouldProcess = this.shouldProcessTask(taskId, taskResult);
if (!shouldProcess) {
return;
}
// Get the task object
const task = this.getTaskById(taskId);
// Skip suite-level tasks - only process individual tests
if (task && task.type === 'suite') {
this.logger.debug(`Skipping suite-level task: ${taskId} (${task.name})`);
return;
}
this.logger.debug(`Processing TaskId: ${taskId}`);
// Log task details for debugging
this.logTaskDetails(task, taskResult);
const testResult = TestResultMapper.mapVitestResult(taskId, taskResult, task);
this.logger.debug(`Mapped test result:`, {
name: testResult.name,
suite_name: testResult.suite_name,
status: testResult.status
});
// Submit test case
try {
await sessionManager.submitTestCase(testResult);
this.logger.debug(`Successfully submitted: ${testResult.name}`);
this.processedTests.add(taskId);
} catch (error) {
this.logger.debug(`Failed to submit (expected if no config): ${testResult.name}`);
// Don't mark as framework error for expected submission failures
// (when running without proper config)
// Don't re-throw to avoid breaking the test runner
}
}
// Determine if a task should be processed
shouldProcessTask(taskId, taskResult) {
// Process completed tests (pass/fail/skip states)
if (taskResult.state === 'pass' || taskResult.state === 'fail' || taskResult.state === 'skip') {
return true;
}
// Check if this is a skipped test by looking at the task object
const task = this.getTaskById(taskId);
if (task && task.mode === 'skip') {
// Create a synthetic taskResult for skipped tests if needed
if (!taskResult.state) {
Object.assign(taskResult, {
state: 'skip',
duration: 0,
errors: []
});
}
return true;
}
return false;
}
// Get task by ID with fallback to context
getTaskById(taskId) {
let task = this.tasks.get(taskId);
// Try to get task from context if not found
if (!task && this.ctx && this.ctx.state) {
if (this.ctx.state.idMap && this.ctx.state.idMap.has(taskId)) {
task = this.ctx.state.idMap.get(taskId);
this.logger.debug(`Found task ${taskId} in context.state.idMap`);
} else if (this.ctx.state.taskMap && this.ctx.state.taskMap.has(taskId)) {
task = this.ctx.state.taskMap.get(taskId);
this.logger.debug(`Found task ${taskId} in context.state.taskMap`);
}
}
return task;
}
// Log task details for debugging
logTaskDetails(task, taskResult) {
if (task) {
this.logger.debug(`Task object found:`, {
id: task.id,
name: task.name,
file: task.file,
type: task.type,
mode: task.mode,
filepath: task.filepath,
projectName: task.projectName,
suite: task.suite ? {
id: task.suite.id,
name: task.suite.name,
file: task.suite.file
} : null
});
} else {
this.logger.debug(`Task object: NOT FOUND`);
}
this.logger.debug(`TaskResult:`, {
state: taskResult.state,
duration: taskResult.duration,
errors: taskResult.errors?.length || 0
});
}
// Process skipped tests in onFinished since they don't trigger onTaskUpdate
async onFinished() {
this.logger.debug('Test run completed, processing any remaining skipped tests');
// Find and process all skipped tests that haven't been processed yet
for (const [taskId, task] of this.tasks) {
if (task && task.mode === 'skip' && task.type === 'test' && !this.processedTests.has(taskId)) {
this.logger.debug(`Processing skipped test: ${taskId} (${task.name})`);
const taskResult = {
state: 'skip',
duration: 0,
errors: []
};
await this.processSkippedTest(taskId, taskResult, task);
}
}
// Clear tasks to free memory
this.tasks.clear();
this.processedTests.clear();
this.logger.debug('Test run finished, cleaned up memory');
}
async processSkippedTest(taskId, taskResult, task) {
this.logger.debug(`Processing skipped TaskId: ${taskId}`);
this.logTaskDetails(task, taskResult);
const testResult = TestResultMapper.mapVitestResult(taskId, taskResult, task);
this.logger.debug(`Mapped test result:`, {
name: testResult.name,
suite_name: testResult.suite_name,
status: testResult.status
});
try {
await sessionManager.submitTestCase(testResult);
this.logger.debug(`Successfully submitted skipped test: ${testResult.name}`);
this.processedTests.add(taskId);
} catch (error) {
this.logger.debug(`Failed to submit skipped test (expected if no config): ${testResult.name}`);
// Don't mark as framework error for expected submission failures
// (when running without proper config)
}
}
}
module.exports = VitestReporter;