UNPKG

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
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;