diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 9bbf156..5c3b05b 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -395,12 +395,6 @@ local function run_command(command, opts) if not results then return end - local lines = format_results(results) - if #lines == 0 then - return - end - ensure_output_started() - append_lines(buf, lines) had_parsed_output = true if options.track_scope then if results.failures_all ~= nil then @@ -409,6 +403,12 @@ local function run_command(command, opts) state.last_scope_failures = results.failures end end + local lines = format_results(results) + if #lines == 0 then + return + end + ensure_output_started() + append_lines(buf, lines) end run_cmd(cmd, cwd, { diff --git a/lua/test-samurai/runners/js.lua b/lua/test-samurai/runners/js.lua index e864da3..f3c51f6 100644 --- a/lua/test-samurai/runners/js.lua +++ b/lua/test-samurai/runners/js.lua @@ -445,7 +445,31 @@ function M.new(opts) local function parse_mocha(output) local ok, data = pcall(vim.json.decode, output) if not ok or type(data) ~= "table" then - return nil + local passes = {} + local failures = {} + local skips = {} + for line in (output or ""):gmatch("[^\n]+") do + local ok_line, entry = pcall(vim.json.decode, line) + if ok_line and type(entry) == "table" then + local event = entry.event or entry[1] or entry["1"] + local payload = entry + if not entry.event then + payload = entry[2] or entry["2"] or {} + end + local title = payload.fullTitle or payload.title + if event == "pass" and title then + table.insert(passes, title) + elseif event == "fail" and title then + table.insert(failures, title) + elseif event == "pending" and title then + table.insert(skips, title) + end + end + end + if #passes == 0 and #failures == 0 and #skips == 0 then + return nil + end + return { passes = passes, failures = failures, skips = skips } end local passes = {} local failures = {} @@ -496,7 +520,7 @@ function M.new(opts) end function runner.output_parser() - local state = { raw = {}, done = false } + local state = { raw = {}, done = false, saw_stream = false } local failures = {} local skips = {} return { @@ -514,28 +538,45 @@ function M.new(opts) end if uses_stream then local ok, data = pcall(vim.json.decode, line) - if ok and type(data) == "table" and data.event then - if data.event == "pass" and data.fullTitle then + if ok and type(data) == "table" then + local event = data.event + local payload = data + if not event then + event = data[1] or data["1"] + payload = data[2] or data["2"] or {} + end + if event == "pass" and payload.fullTitle then + state.saw_stream = true return { - passes = { data.fullTitle }, + passes = { payload.fullTitle }, failures = {}, skips = {}, failures_all = vim.deepcopy(failures), } - elseif data.event == "fail" and data.fullTitle then - table.insert(failures, data.fullTitle) + elseif event == "fail" and payload.fullTitle then + state.saw_stream = true + table.insert(failures, payload.fullTitle) return { passes = {}, - failures = { data.fullTitle }, + failures = { payload.fullTitle }, skips = {}, failures_all = vim.deepcopy(failures), } - elseif data.event == "pending" and data.fullTitle then - table.insert(skips, data.fullTitle) + elseif event == "pending" and payload.fullTitle then + state.saw_stream = true + table.insert(skips, payload.fullTitle) return { passes = {}, failures = {}, - skips = { data.fullTitle }, + skips = { payload.fullTitle }, + failures_all = vim.deepcopy(failures), + } + elseif event == "start" or event == "end" then + state.saw_stream = true + return { + passes = {}, + failures = {}, + skips = {}, failures_all = vim.deepcopy(failures), } end @@ -596,7 +637,7 @@ function M.new(opts) return results end, on_complete = function(output, _state) - if state.done then + if state.done or state.saw_stream then return nil end local results = parse_output(output) @@ -618,8 +659,13 @@ function M.new(opts) append_args(cmd, runner.json_args) if runner.framework == "mocha" then - table.insert(cmd, "--grep") - table.insert(cmd, pattern) + if #failures == 1 then + table.insert(cmd, "--fgrep") + table.insert(cmd, failures[1]) + else + table.insert(cmd, "--grep") + table.insert(cmd, pattern) + end else table.insert(cmd, "-t") table.insert(cmd, pattern) diff --git a/tests/test_samurai_failed_only_spec.lua b/tests/test_samurai_failed_only_spec.lua index b5c4b00..cabb9af 100644 --- a/tests/test_samurai_failed_only_spec.lua +++ b/tests/test_samurai_failed_only_spec.lua @@ -221,6 +221,77 @@ describe("TSamFailedOnly", function() assert.is_false(has_raw) end) + it("reruns failed mocha tests from json-stream array output without raw JSON", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.js-mocha", + }, + }) + + local fail_line = vim.json.encode({ + event = "fail", + fullTitle = "API :: /brands... GET: /", + }) + local start_line = vim.json.encode({ "start", { total = 1 } }) + local end_line = vim.json.encode({ "end", { tests = 0 } }) + + local calls, orig_jobstart = stub_jobstart({ + exit_codes = { 1, 1 }, + stdout = { { fail_line }, { start_line, end_line } }, + }) + + local bufnr = mkbuf("/tmp/project/brands.test.js", "javascript", { + 'describe("API :: /brands...", function() {', + ' it("GET: /", function() {', + " -- inside test", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_file() + core.run_failed_only() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + + assert.equals(2, #calls) + assert.are.same( + { "npx", "mocha", "--reporter", "json-stream", "/tmp/project/brands.test.js" }, + calls[1].cmd + ) + local failed_cmd = calls[2].cmd or {} + local saw_grep = false + local saw_fgrep = false + local saw_title = false + local plain_title = "API :: /brands... GET: /" + for _, arg in ipairs(failed_cmd) do + if arg == "--grep" then + saw_grep = true + elseif arg == "--fgrep" then + saw_fgrep = true + elseif arg == plain_title then + saw_title = true + end + end + assert.is_false(saw_grep) + assert.is_true(saw_fgrep) + assert.is_true(saw_title) + + local has_raw = false + for _, line in ipairs(lines) do + if line == start_line or line == end_line then + has_raw = true + break + end + end + assert.is_false(has_raw) + end) + it("does not affect TSamLast history", function() local json = vim.json.encode({ testResults = { diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index fa5945a..881bc7d 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -273,6 +273,67 @@ describe("test-samurai output formatting", function() assert.is_false(has_raw_json) end) + it("formats mocha json-stream array output as PASS/FAIL lines", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.js-mocha", + }, + }) + + local pass_line = vim.json.encode({ + "pass", + { + title = "GET: /", + fullTitle = "API :: /brands... GET: /", + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { pass_line }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_mocha_json_stream.test.js") + vim.bo[bufnr].filetype = "javascript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_nearest() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + + local has_pass = false + local has_raw_json = false + for _, line in ipairs(lines) do + if line == "[ PASS ] - API :: /brands... GET: /" then + has_pass = true + elseif line == pass_line then + has_raw_json = true + end + end + + assert.is_true(has_pass) + assert.is_false(has_raw_json) + end) + it("does not print raw JSON when JSON arrives on stdout and stderr", function() test_samurai.setup({ runner_modules = { @@ -426,15 +487,19 @@ describe("test-samurai output formatting", function() assert.is_true(#job_calls >= 2) local failed_cmd = job_calls[2].cmd or {} local saw_grep = false + local saw_fgrep = false local saw_title = false for _, arg in ipairs(failed_cmd) do if arg == "--grep" then saw_grep = true + elseif arg == "--fgrep" then + saw_fgrep = true elseif arg == "outer inner 2" then saw_title = true end end - assert.is_true(saw_grep) + assert.is_false(saw_grep) + assert.is_true(saw_fgrep) assert.is_true(saw_title) end)