/** * test_samurai_vitest_reporter.mjs * * Custom Vitest 2.x reporter for test-samurai. * * Emits one line per completed test case to stdout: * TSAMURAI_RESULT * * Payload schema: * { * name: string -- Describe-hierarchy + test title joined with "/" * status: "passed" | "failed" | "skipped" * file: string | null -- absolute file path * location: { line: number, column: number } | null * output: string[] -- error messages / stack traces (failures only) * } */ const RESULT_PREFIX = 'TSAMURAI_RESULT '; export default class TestSamuraiVitestReporter { onTestCaseResult(testCase) { const name = buildListingName(testCase); const status = mapStatus(testCase.task); if (!status) return; const output = buildOutput(testCase.task); const payload = { name, status, file: testCase.file?.filepath ?? null, location: testCase.location ?? null, output, }; process.stdout.write(`${RESULT_PREFIX}${JSON.stringify(payload)}\n`); } } /** * Builds a slash-separated name from the describe hierarchy. * E.g. describe('MyComponent') > describe('renders') > it('the button') * → "MyComponent/renders/the button" */ function buildListingName(testCase) { const parts = [testCase.name]; let current = testCase.parent; while (current && current.type === 'suite') { parts.unshift(current.name); current = current.parent; } return parts.filter(Boolean).join('/'); } /** * Maps Vitest task result states to test-samurai status strings. * Falls back to task.mode if result.state is missing. */ function mapStatus(task) { if (!task) return null; const state = task.result?.state; if (state === 'pass') return 'passed'; if (state === 'fail') return 'failed'; if (state === 'skip') return 'skipped'; const mode = task.mode; if (mode === 'skip' || mode === 'todo') return 'skipped'; return null; } /** * Collects error messages and stack traces for failed tests. */ function buildOutput(task) { const lines = []; if (!task) return lines; const errors = task.result?.errors ?? []; for (const err of errors) { if (typeof err.message === 'string' && err.message.length > 0) { err.message.split('\n').forEach((line) => { if (line !== '') lines.push(line); }); } if (typeof err.stack === 'string' && err.stack.length > 0) { err.stack.split('\n').forEach((line) => { if (line !== '') lines.push(line); }); } } return lines; }