'use strict'; function reqFromCwd(id) { return require(require.resolve(id, { paths: [process.cwd()] })); } const Mocha = reqFromCwd('mocha'); const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_SUITE_BEGIN, EVENT_TEST_BEGIN, EVENT_TEST_END, EVENT_TEST_PASS, EVENT_TEST_FAIL, EVENT_TEST_PENDING, EVENT_HOOK_FAIL, } = Mocha.Runner.constants; function serializeError(err) { if (!err) return null; return { name: err.name || null, message: err.message || String(err), stack: err.stack || null, }; } function safeTitlePath(entity) { if (entity && typeof entity.titlePath === 'function') { return entity.titlePath(); } return null; } class NdjsonReporter { constructor(runner /*, options */) { this.currentTest = null; this.stdoutByTest = new WeakMap(); this.originalStdoutWrite = process.stdout.write.bind(process.stdout); process.stdout.write = (...args) => { this.captureStdout(args[0]); return this.originalStdoutWrite(...args); }; runner .once(EVENT_RUN_BEGIN, () => { this.emit({ event: 'run-begin', total: runner.total }); }) .on(EVENT_TEST_BEGIN, (test) => { this.currentTest = test; }) .on(EVENT_TEST_END, (test) => { if (this.currentTest === test) { this.currentTest = null; } }) // Optional suite-level event for describe.skip (helps jump to suite definition) .on(EVENT_SUITE_BEGIN, (suite) => { if (suite && suite.pending && suite.title) { this.emit({ event: 'suite', status: 'skipped', pendingType: suite._akPendingType || 'skip', title: suite.title, fullTitle: typeof suite.fullTitle === 'function' ? suite.fullTitle() : suite.title, titlePath: safeTitlePath(suite), file: suite.file || (suite._akLocation && suite._akLocation.file) || null, location: suite._akLocation || null, }); } }) .on(EVENT_TEST_PASS, (test) => { this.emit(this.testPayload('passed', test, null)); }) .on(EVENT_TEST_FAIL, (test, err) => { this.emit(this.testPayload('failed', test, serializeError(err))); }) .on(EVENT_HOOK_FAIL, (hook, err) => { this.emit(this.hookPayload('failed', hook, serializeError(err))); }) .on(EVENT_TEST_PENDING, (test) => { // Mocha "pending" includes: // - TODO tests (no function) // - explicit it.skip / describe.skip // - runtime this.skip() // We'll classify: // - pendingType === 'todo' => pending // - otherwise => skipped const inferredPendingType = this.inferPendingType(test); const status = inferredPendingType === 'todo' ? 'pending' : 'skipped'; this.emit({ ...this.testPayload(status, test, null), pendingType: inferredPendingType, }); }) .once(EVENT_RUN_END, () => { process.stdout.write = this.originalStdoutWrite; this.emit({ event: 'run-end', stats: runner.stats || null }); }); } captureStdout(chunk) { if (!this.currentTest) return; const text = Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk); if (!this.stdoutByTest.has(this.currentTest)) { this.stdoutByTest.set(this.currentTest, []); } this.stdoutByTest.get(this.currentTest).push(text); } consumeStdoutLines(test) { const chunks = this.stdoutByTest.get(test); if (!chunks) return null; this.stdoutByTest.delete(test); const text = chunks.join(''); if (!text) return null; const lines = text.split(/\r?\n/); if (lines.length > 0 && lines[lines.length - 1] === '') { lines.pop(); } return lines.length > 0 ? lines : null; } inferPendingType(test) { // Priority: explicit marker from UI wrappers if (test && test._akPendingType) return test._akPendingType; // Heuristic: // - If there's no function (or it's missing), it's likely a TODO-style pending // - If function exists but test ended as pending, it's likely skipped (this.skip()) if (!test) return null; // In many Mocha versions, TODO tests have test.fn undefined/null if (!test.fn) return 'todo'; return 'skip'; } testPayload(status, test, errorObj) { const location = (test && test._akLocation) || null; const file = (test && test.file) || (test && test.parent && test.parent.file) || (location && location.file) || null; const payload = { event: 'test', status, title: test ? test.title : null, fullTitle: test && typeof test.fullTitle === 'function' ? test.fullTitle() : null, titlePath: safeTitlePath(test), file, location, duration: test && typeof test.duration === 'number' ? test.duration : null, currentRetry: test && typeof test.currentRetry === 'function' ? test.currentRetry() : null, }; const output = this.consumeStdoutLines(test); if (output && output.length > 0) payload.output = output; if (status === 'failed') payload.error = errorObj; return payload; } hookPayload(status, hook, errorObj) { const currentTest = hook && hook.ctx && hook.ctx.currentTest ? hook.ctx.currentTest : null; const hookType = hook && hook.type ? hook.type : 'hook'; const titlePath = (currentTest && safeTitlePath(currentTest)) || (hook && hook.parent && safeTitlePath(hook.parent)) || safeTitlePath(hook); const fullTitle = (currentTest && typeof currentTest.fullTitle === 'function' && currentTest.fullTitle()) || (hook && hook.title) || null; const file = (hook && hook.file) || (currentTest && currentTest.file) || (hook && hook.parent && hook.parent.file) || null; const location = (currentTest && currentTest._akLocation) || (hook && hook._akLocation) || null; const payload = { event: 'hook', status, hookType, title: hook && hook.title ? hook.title : null, fullTitle, titlePath, file, location, }; if (status === 'failed') payload.error = errorObj; return payload; } emit(obj) { this.originalStdoutWrite(JSON.stringify(obj) + '\n'); } } module.exports = NdjsonReporter;