/** * test_samurai_vitest_reporter.js * * 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.result?.state); const output = buildOutput(testCase); 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 result states to test-samurai status strings. */ function mapStatus(state) { const map = { pass: 'passed', fail: 'failed', skip: 'skipped', todo: 'skipped', }; return map[state] ?? 'skipped'; } /** * Collects error messages and stack traces for failed tests. */ function buildOutput(testCase) { const lines = []; const errors = testCase.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; }