UNPKG

fern-jest-client

Version:

Jest test reporter client for Fern platform - captures and sends test execution data to fern-reporter

221 lines 8.15 kB
"use strict"; /** * Mapper utilities to convert Jest test results to Fern data models */ Object.defineProperty(exports, "__esModule", { value: true }); exports.mapJestResultsToTestRun = mapJestResultsToTestRun; exports.mapJestSuiteToSuiteRun = mapJestSuiteToSuiteRun; exports.mapJestTestToSpecRun = mapJestTestToSpecRun; exports.mapTestRunToCreateInput = mapTestRunToCreateInput; exports.generateRunId = generateRunId; exports.generateTestSummary = generateTestSummary; /** * Convert Jest aggregated results to Fern TestRun */ function mapJestResultsToTestRun(results, projectId, projectName, gitInfo, ciInfo) { const startTime = new Date(results.startTime).toISOString(); const endTime = new Date().toISOString(); // Jest doesn't provide end time, use current time // Use nanosecond precision timestamp as test seed for uniqueness // process.hrtime.bigint() returns nanoseconds since an arbitrary time // We'll use current timestamp in milliseconds * 1000000 + nanosecond component const hrTime = process.hrtime.bigint(); const nanoTime = Number(hrTime % 1000000n); // Get last 6 digits of nanoseconds const testSeed = Date.now() * 1000000 + nanoTime; const testRunId = testSeed; // Use same value for consistency const suiteRuns = results.testResults.map((suite, index) => mapJestSuiteToSuiteRun(suite, testRunId, index + 1)); return { id: testRunId, test_project_name: projectName, test_project_id: projectId, test_seed: testSeed, start_time: startTime, end_time: endTime, git_branch: gitInfo.branch, git_sha: gitInfo.sha, build_trigger_actor: ciInfo.actor, build_url: ciInfo.buildUrl, client_type: 'fern-jest-client', suite_runs: suiteRuns }; } /** * Convert Jest test suite result to Fern SuiteRun */ function mapJestSuiteToSuiteRun(suite, testRunId, suiteId) { const suiteName = extractSuiteName(suite.testFilePath); const startTime = suite.startTime ? new Date(suite.startTime).toISOString() : new Date().toISOString(); const endTime = suite.endTime ? new Date(suite.endTime).toISOString() : new Date().toISOString(); const specRuns = suite.testResults.map((test, index) => mapJestTestToSpecRun(test, suite.startTime || Date.now(), suiteId, index + 1)); return { id: suiteId, test_run_id: testRunId, suite_name: suiteName, start_time: startTime, end_time: endTime, spec_runs: specRuns }; } /** * Convert Jest test result to Fern SpecRun */ function mapJestTestToSpecRun(test, suiteStartTime, suiteId, specId) { const specDescription = buildSpecDescription(test); const status = mapJestStatusToFernStatus(test.status); const message = extractFailureMessage(test); const tags = extractTagsFromTest(test); // Calculate timing - Jest doesn't provide individual test start times const testDuration = test.duration || 0; const startTime = new Date(suiteStartTime).toISOString(); const endTime = new Date(suiteStartTime + testDuration).toISOString(); return { id: specId, suite_id: suiteId, spec_description: specDescription, status: status, message: message, tags: tags, start_time: startTime, end_time: endTime }; } /** * Extract suite name from file path */ function extractSuiteName(filePath) { const fileName = filePath.split('/').pop() || filePath; return fileName.replace(/\.(test|spec)\.(js|ts|jsx|tsx)$/, ''); } /** * Build full spec description including ancestor titles */ function buildSpecDescription(test) { const ancestors = test.ancestorTitles.join(' > '); const fullDescription = ancestors ? `${ancestors} > ${test.title}` : test.title; return fullDescription; } /** * Map Jest status to Fern status */ function mapJestStatusToFernStatus(jestStatus) { switch (jestStatus) { case 'passed': return 'passed'; case 'failed': return 'failed'; case 'skipped': case 'pending': case 'disabled': return 'skipped'; case 'todo': return 'pending'; default: return 'unknown'; } } /** * Extract failure message from test result */ function extractFailureMessage(test) { if (test.status === 'passed') { return ''; } if (test.failureMessages && test.failureMessages.length > 0) { return test.failureMessages.join('\n'); } if (test.failureDetails && test.failureDetails.length > 0) { return test.failureDetails.map(detail => detail.message).join('\n'); } return test.status === 'failed' ? 'Test failed' : ''; } /** * Extract tags from test metadata * Jest doesn't have native tagging, but we can extract from test names or file paths */ function extractTagsFromTest(test) { const tags = []; let tagId = 1; // Extract tags from test title (e.g., "should work [unit]" or "@unit should work") const titleTags = extractTagsFromString(test.title); tags.push(...titleTags); // Extract tags from ancestor titles test.ancestorTitles.forEach(ancestor => { const ancestorTags = extractTagsFromString(ancestor); tags.push(...ancestorTags); }); // Add default tag if no tags found if (tags.length === 0) { tags.push({ id: tagId++, name: 'default' }); } // Remove duplicates and assign IDs const uniqueTagNames = new Set(); const uniqueTags = []; tags.forEach(tag => { if (!uniqueTagNames.has(tag.name)) { uniqueTagNames.add(tag.name); uniqueTags.push({ id: tagId++, name: tag.name }); } }); return uniqueTags; } /** * Extract tags from a string using common patterns */ function extractTagsFromString(str) { const tags = []; // Pattern 1: [tag] or [tag1,tag2] const bracketPattern = /\[([^\]]+)\]/g; let bracketMatch; while ((bracketMatch = bracketPattern.exec(str)) !== null) { const tagNames = bracketMatch[1].split(',').map(t => t.trim()); tagNames.forEach(name => { if (name) tags.push({ id: 0, name }); // ID will be assigned later }); } // Pattern 2: @tag or @tag1 @tag2 const atPattern = /@([a-zA-Z0-9_-]+)/g; let atMatch; while ((atMatch = atPattern.exec(str)) !== null) { tags.push({ id: 0, name: atMatch[1] }); } // Pattern 3: #tag or #tag1 #tag2 const hashPattern = /#([a-zA-Z0-9_-]+)/g; let hashMatch; while ((hashMatch = hashPattern.exec(str)) !== null) { tags.push({ id: 0, name: hashMatch[1] }); } return tags; } /** * Convert TestRun to CreateTestRunInput for API submission * Since TestRun now has all required fields, we just return it as-is */ function mapTestRunToCreateInput(testRun) { return testRun; } /** * Generate unique run ID */ function generateRunId() { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `jest-${timestamp}-${random}`; } /** * Generate test run summary for logging */ function generateTestSummary(testRun) { const totalSpecs = testRun.suite_runs.reduce((sum, suite) => sum + suite.spec_runs.length, 0); const passedSpecs = testRun.suite_runs.reduce((sum, suite) => sum + suite.spec_runs.filter(spec => spec.status === 'passed').length, 0); const failedSpecs = testRun.suite_runs.reduce((sum, suite) => sum + suite.spec_runs.filter(spec => spec.status === 'failed').length, 0); const skippedSpecs = testRun.suite_runs.reduce((sum, suite) => sum + suite.spec_runs.filter(spec => spec.status === 'skipped').length, 0); return [ `Fern Test Run Summary:`, ` Project: ${testRun.test_project_id}`, ` Branch: ${testRun.git_branch}`, ` Commit: ${testRun.git_sha.substring(0, 8)}`, ` Suites: ${testRun.suite_runs.length}`, ` Tests: ${totalSpecs} total, ${passedSpecs} passed, ${failedSpecs} failed, ${skippedSpecs} skipped` ].join('\n'); } //# sourceMappingURL=mapper.js.map