UNPKG

js-tests-results-collector

Version:

Universal test results collector for Jest, Jasmine, Mocha, Cypress, and Playwright that sends results to Buddy Works API

265 lines (232 loc) 9.29 kB
class TestResultMapper { // Helper function to convert data object to XML static toXml(obj) { let xml = '<data>'; for (const [key, value] of Object.entries(obj)) { if (value) { xml += `<${key}><![CDATA[${value}]]></${key}>`; } else { xml += `<${key}></${key}>`; } } xml += '</data>'; return xml; } static mapJestResult(testResult, suiteResult) { // Debug logging to see what Jest sends for skipped tests const Logger = require('./logger'); const logger = new Logger('TestResultMapper'); if (testResult.status !== 'passed' && testResult.status !== 'failed') { logger.debug(`Jest test status debug:`, { title: testResult.title, status: testResult.status, invocations: testResult.invocations, numPassingAsserts: testResult.numPassingAsserts, duration: testResult.duration, pending: testResult.pending, todo: testResult.todo, skip: testResult.skip }); } // Enhanced skipped test detection let status; // Check if test was skipped based on multiple Jest properties const isSkipped = testResult.status === 'skipped' || testResult.status === 'pending' || testResult.status === 'todo' || testResult.status === 'disabled' || testResult.status === 'focused' || testResult.status === 'skip' || testResult.pending === true || testResult.todo === true || testResult.skip === true || (testResult.invocations === 0 && testResult.status !== 'failed'); if (testResult.status === 'passed') { status = 'PASSED'; } else if (testResult.status === 'failed') { status = 'FAILED'; } else if (isSkipped) { status = 'SKIPPED'; } else { status = 'ERROR'; } // Create data object similar to BuddyWorks.Nunit.TestLogger const dataObj = { errorMessage: testResult.failureMessages?.length > 0 ? testResult.failureMessages.join('\n') : '', errorStackTrace: testResult.failureMessages?.length > 0 ? testResult.failureMessages.join('\n') : '', messages: testResult.ancestorTitles?.join(' > ') || '' }; const fileName = suiteResult.testFilePath.split('/').pop(); const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, ""); return { name: testResult.title, classname: fileNameWithoutExt, suite_name: fileNameWithoutExt, // Fixed: Only send suite name status: status, time: testResult.duration ? testResult.duration / 1000 : 0, data: this.toXml(dataObj) }; } static mapJasmineResult(result) { const status = result.status === 'passed' ? 'PASSED' : result.status === 'failed' ? 'FAILED' : result.status === 'pending' ? 'SKIPPED' : 'ERROR'; // Create data object similar to BuddyWorks.Nunit.TestLogger const dataObj = { errorMessage: result.failedExpectations?.map(exp => exp.message).join('\n') || '', errorStackTrace: result.failedExpectations?.map(exp => exp.stack).join('\n') || '', messages: result.fullName || '' }; const suiteName = result.fullName.split(' ')[0]; return { name: result.description, classname: suiteName, suite_name: suiteName, // Fixed: Only send suite name status: status, time: (result.endTime - result.startTime) / 1000 || 0, data: this.toXml(dataObj) }; } static mapMochaResult(test) { const status = test.state === 'passed' ? 'PASSED' : test.state === 'failed' ? 'FAILED' : test.pending ? 'SKIPPED' : 'ERROR'; // Create data object similar to BuddyWorks.Nunit.TestLogger const dataObj = { errorMessage: test.err ? test.err.message : '', errorStackTrace: test.err ? test.err.stack : '', messages: test.fullTitle?.() || '' }; const suiteName = test.parent?.title || test.file || 'Unknown Suite'; return { name: test.title, classname: suiteName, suite_name: suiteName, // Fixed: Only send suite name status: status, time: test.duration ? test.duration / 1000 : 0, data: this.toXml(dataObj) }; } static mapCypressResult(test) { const status = test.state === 'passed' ? 'PASSED' : test.state === 'failed' ? 'FAILED' : test.state === 'skipped' ? 'SKIPPED' : test.pending ? 'SKIPPED' : 'ERROR'; // Create data object similar to BuddyWorks.Nunit.TestLogger const dataObj = { errorMessage: test.err ? test.err.message : '', errorStackTrace: test.err ? test.err.stack : '', messages: test.body || '' }; const suiteName = test.parent?.title || 'Cypress Suite'; return { name: test.title, classname: suiteName, suite_name: suiteName, // Fixed: Only send suite name status: status, time: test.duration ? test.duration / 1000 : 0, data: this.toXml(dataObj) }; } static mapPlaywrightResult(test, result) { const status = result.status === 'passed' ? 'PASSED' : result.status === 'failed' ? 'FAILED' : result.status === 'skipped' ? 'SKIPPED' : result.status === 'timedOut' ? 'FAILED' : 'ERROR'; // Create data object similar to BuddyWorks.Nunit.TestLogger const dataObj = { errorMessage: result.error ? result.error.message : '', errorStackTrace: result.error ? result.error.stack : '', messages: test.location?.file || '' }; const suiteName = test.parent?.title || test.location?.file?.split('/').pop() || 'Playwright Suite'; return { name: test.title, classname: suiteName, suite_name: suiteName, // Fixed: Only send suite name status: status, time: result.duration ? result.duration / 1000 : 0, data: this.toXml(dataObj) }; } static mapVitestResult(taskId, taskResult, task = null) { const status = taskResult.state === 'pass' ? 'PASSED' : taskResult.state === 'fail' ? 'FAILED' : taskResult.state === 'skip' ? 'SKIPPED' : 'ERROR'; // Create data object similar to BuddyWorks.Nunit.TestLogger const dataObj = { errorMessage: taskResult.errors?.length > 0 ? taskResult.errors.map(e => e.message).join('\n') : '', errorStackTrace: taskResult.errors?.length > 0 ? taskResult.errors.map(e => e.stack).join('\n') : '', messages: taskResult.name || taskId || '' }; // Extract test and suite names - prioritize actual task object if available let testName = 'Unknown Test'; let suiteName = 'Unknown Suite'; if (task) { // Use actual task object to get proper names testName = task.name || task.title || taskId || 'Unknown Test'; // Try to get suite name from parent or file if (task.suite && task.suite.name) { suiteName = task.suite.name; } else if (task.file) { // Extract filename from path - handle both string and object cases let filePath = ''; if (typeof task.file === 'string') { filePath = task.file; } else if (task.file && task.file.filepath) { filePath = task.file.filepath; } else if (task.file && task.file.name) { filePath = task.file.name; } if (filePath) { const fileName = filePath.split('/').pop(); suiteName = fileName.replace(/\.(test|spec)\.(js|ts|jsx|tsx)$/, ''); } else { suiteName = 'Vitest Suite'; } } else { suiteName = 'Vitest Suite'; } } else { // Fallback: Extract test and suite names from taskResult only if (taskResult.name) { testName = taskResult.name; } else if (taskId) { testName = taskId; } // Extract suite name from various sources if (taskResult.suite?.name) { // Direct suite name from taskResult suiteName = taskResult.suite.name; } else if (taskResult.file) { // Extract suite name from file path const fileName = taskResult.file.split('/').pop(); suiteName = fileName.replace(/\.(test|spec)\.(js|ts|jsx|tsx)$/, ''); } else if (taskResult.location?.file) { // Extract from location.file const fileName = taskResult.location.file.split('/').pop(); suiteName = fileName.replace(/\.(test|spec)\.(js|ts|jsx|tsx)$/, ''); } else if (taskResult.filepath) { // Extract from filepath const fileName = taskResult.filepath.split('/').pop(); suiteName = fileName.replace(/\.(test|spec)\.(js|ts|jsx|tsx)$/, ''); } else { // Default fallback suiteName = 'Vitest Suite'; } } return { name: testName, classname: suiteName, suite_name: suiteName, status: status, time: taskResult.duration ? taskResult.duration / 1000 : 0, data: this.toXml(dataObj) }; } } module.exports = TestResultMapper;