diff --git a/README.md b/README.md index 9443573..cb4428f 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ which emits one JSON object per line: "titlePath": ["Parent", "Child", "Test"], "file": "/path/to/test.js", "location": { "file": "/path/to/test.js", "line": 10, "column": 5 }, + "output": ["stdout line 1", "stdout line 2"], "error": { "message": "string", "stack": "string" } } ``` diff --git a/lua/test-samurai-mocha-runner/init.lua b/lua/test-samurai-mocha-runner/init.lua index 3d1f329..ccf2112 100644 --- a/lua/test-samurai-mocha-runner/init.lua +++ b/lua/test-samurai-mocha-runner/init.lua @@ -248,6 +248,35 @@ local function build_error_lines(error_obj) return lines end +local function output_lines_from_payload(payload) + local output = payload and payload.output + if type(output) == "string" then + return vim.split(output, "\n", { plain = true, trimempty = false }) + end + if type(output) == "table" then + local lines = {} + for _, line in ipairs(output) do + if type(line) == "string" then + if line:find("\n", 1, true) then + for _, split_line in ipairs(vim.split(line, "\n", { plain = true, trimempty = false })) do + table.insert(lines, split_line) + end + else + table.insert(lines, line) + end + end + end + return lines + end + return {} +end + +local function append_lines(target, lines) + for _, line in ipairs(lines) do + table.insert(target, line) + end +end + local function extract_location(stack) if not stack then return nil @@ -356,8 +385,11 @@ local function record_ndjson_result(state, status, payload) end local location = normalize_location(payload.location) + local output_lines = output_lines_from_payload(payload) if status == "failed" then - local lines = build_error_lines(payload.error) + local lines = {} + append_lines(lines, output_lines) + append_lines(lines, build_error_lines(payload.error)) if #lines > 0 then state.outputs[full_name] = lines end @@ -377,6 +409,13 @@ local function record_ndjson_result(state, status, payload) location.text = status state.locations[full_name] = location end + if #output_lines > 0 then + state.outputs[full_name] = output_lines + end + end + + if (not state.outputs[full_name] or #state.outputs[full_name] == 0) and status ~= "failed" then + state.outputs[full_name] = { status } end if added then diff --git a/scripts/mocha-ndjson-reporter.cjs b/scripts/mocha-ndjson-reporter.cjs index 6d0291d..8498952 100644 --- a/scripts/mocha-ndjson-reporter.cjs +++ b/scripts/mocha-ndjson-reporter.cjs @@ -9,6 +9,8 @@ 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, @@ -32,11 +34,29 @@ function safeTitlePath(entity) { 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) { @@ -78,10 +98,33 @@ class NdjsonReporter { }) .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; @@ -118,13 +161,16 @@ class NdjsonReporter { 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; } emit(obj) { - process.stdout.write(JSON.stringify(obj) + '\n'); + this.originalStdoutWrite(JSON.stringify(obj) + '\n'); } } diff --git a/tests/test_mocha_runner_spec.lua b/tests/test_mocha_runner_spec.lua index 4fa66e8..92463c2 100644 --- a/tests/test_mocha_runner_spec.lua +++ b/tests/test_mocha_runner_spec.lua @@ -280,6 +280,7 @@ describe("test-samurai-mocha-runner", function() titlePath = { "Math", "subs" }, file = "/tmp/math.spec.js", location = { file = "/tmp/math.spec.js", line = 10, column = 5 }, + output = { "log: before", "log: after" }, error = { message = "oops", stack = "Error: oops\n at Context. (/tmp/math.spec.js:10:5)\n at processImmediate", @@ -293,6 +294,7 @@ describe("test-samurai-mocha-runner", function() titlePath = { "Math", "adds" }, file = "/tmp/math.spec.js", location = { file = "/tmp/math.spec.js", line = 4, column = 2 }, + output = { "adds log" }, }), vim.json.encode({ event = "test", @@ -311,6 +313,10 @@ describe("test-samurai-mocha-runner", function() assert.is_true(outputs["Math/subs"] ~= nil) assert.is_true(outputs["Math/adds"] ~= nil) assert.is_true(outputs["Math/skips"] ~= nil) + assert.equals("log: before", outputs["Math/subs"][1]) + assert.equals("log: after", outputs["Math/subs"][2]) + assert.equals("adds log", outputs["Math/adds"][1]) + assert.equals("skipped", outputs["Math/skips"][1]) end) it("does not return results on complete", function()