'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_PASS, EVENT_TEST_FAIL, EVENT_TEST_PENDING, } = 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 */) { runner .once(EVENT_RUN_BEGIN, () => { this.emit({ event: 'run-begin', total: runner.total }); }) // 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_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, () => { this.emit({ event: 'run-end', stats: runner.stats || 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, }; if (status === 'failed') payload.error = errorObj; return payload; } emit(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); } } module.exports = NdjsonReporter;