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