From a6e51f280fc8f226cf70c8d2823d46811d5e7d42 Mon Sep 17 00:00:00 2001 From: "M.Schirmer" Date: Thu, 25 Dec 2025 18:53:31 +0100 Subject: [PATCH] fix TSamFailedOnly and the output formatting for the jest-runner --- .gitignore | 1 + AGENTS.md | 1 + lua/test-samurai/core.lua | 22 +- lua/test-samurai/runners/go.lua | 108 ++++--- lua/test-samurai/runners/js-jest.lua | 2 +- lua/test-samurai/runners/js.lua | 373 +++++++++++++++++++++--- tests/test_samurai_failed_only_spec.lua | 6 +- tests/test_samurai_js_spec.lua | 2 +- tests/test_samurai_last_spec.lua | 2 +- tests/test_samurai_output_spec.lua | 206 ++++++++++++- 10 files changed, 617 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index 53752db..7ff46e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ output +.nvimlog diff --git a/AGENTS.md b/AGENTS.md index 6b12d35..5d10162 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ - Rolle: **TDD-first Entwickler** - Jede neue Funktion, jedes neue Kommando und jede Verhaltensänderung **muss durch Tests abgesichert sein**. - Nach jeder Code-Änderung **Tests via `bash run_test.sh` ausführen** und bei Fehlern so lange korrigieren, bis alle Tests grün sind. +- Antworten immer auf Deutsch. - **Nicht raten**: - Bei unklaren oder mehrdeutigen Anforderungen **Arbeit stoppen** und Klarstellung verlangen. - TODO/NOTE im Code ist zulässig, stilles Raten nicht. diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 5c3b05b..2d69fc3 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -311,20 +311,30 @@ local function run_cmd(cmd, cwd, handlers) }) end +local function pick_display(results, key) + if type(results.display) == "table" and type(results.display[key]) == "table" then + return results.display[key] + end + return results[key] +end + local function format_results(results) local lines = {} - if type(results.passes) == "table" then - for _, title in ipairs(results.passes) do + local passes = pick_display(results, "passes") + if type(passes) == "table" then + for _, title in ipairs(passes) do table.insert(lines, "[ PASS ] - " .. title) end end - if type(results.skips) == "table" then - for _, title in ipairs(results.skips) do + local skips = pick_display(results, "skips") + if type(skips) == "table" then + for _, title in ipairs(skips) do table.insert(lines, "[ SKIP ] - " .. title) end end - if type(results.failures) == "table" then - for _, title in ipairs(results.failures) do + local failures = pick_display(results, "failures") + if type(failures) == "table" then + for _, title in ipairs(failures) do table.insert(lines, "[ FAIL ] - " .. title) end end diff --git a/lua/test-samurai/runners/go.lua b/lua/test-samurai/runners/go.lua index 42d4981..60ecda6 100644 --- a/lua/test-samurai/runners/go.lua +++ b/lua/test-samurai/runners/go.lua @@ -241,21 +241,28 @@ end function runner.parse_results(output) if not output or output == "" then - return { passes = {}, failures = {}, skips = {} } + return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } } end local passes = {} local failures = {} local skips = {} + local display = { passes = {}, failures = {}, skips = {} } for line in output:gmatch("[^\n]+") do local ok, data = pcall(vim.json.decode, line) if ok and type(data) == "table" then if data.Test and data.Test ~= "" then if data.Action == "pass" then table.insert(passes, data.Test) + local short = data.Test:match("([^/]+)$") or data.Test + table.insert(display.passes, short) elseif data.Action == "fail" then table.insert(failures, data.Test) + local short = data.Test:match("([^/]+)$") or data.Test + table.insert(display.failures, short) elseif data.Action == "skip" then table.insert(skips, data.Test) + local short = data.Test:match("([^/]+)$") or data.Test + table.insert(display.skips, short) end end end @@ -264,56 +271,65 @@ function runner.parse_results(output) passes = collect_unique(passes), failures = collect_unique(failures), skips = collect_unique(skips), + display = display, } end -function runner.output_parser() - local seen_pass = {} - local seen_fail = {} - local failures = {} - local passes = {} - local skips = {} + function runner.output_parser() + local seen_pass = {} + local seen_fail = {} + local failures = {} + local passes = {} + local skips = {} + local display = { passes = {}, failures = {}, skips = {} } - return { - on_line = function(line, _state) - local ok, data = pcall(vim.json.decode, line) - if not ok or type(data) ~= "table" then + return { + on_line = function(line, _state) + local ok, data = pcall(vim.json.decode, line) + if not ok or type(data) ~= "table" then + return nil + end + local name = data.Test + if not name or name == "" then + return nil + end + local short = name:match("([^/]+)$") or name + if data.Action == "pass" and not seen_pass[name] then + seen_pass[name] = true + table.insert(passes, name) + table.insert(display.passes, short) + return { + passes = { name }, + failures = {}, + skips = {}, + display = { passes = { short }, failures = {}, skips = {} }, + failures_all = vim.deepcopy(failures), + } + elseif data.Action == "fail" and not seen_fail[name] then + seen_fail[name] = true + table.insert(failures, name) + table.insert(display.failures, short) + return { + passes = {}, + failures = { name }, + skips = {}, + display = { passes = {}, failures = { short }, skips = {} }, + failures_all = vim.deepcopy(failures), + } + elseif data.Action == "skip" and not seen_pass[name] then + seen_pass[name] = true + table.insert(skips, name) + table.insert(display.skips, short) + return { + passes = {}, + failures = {}, + skips = { name }, + display = { passes = {}, failures = {}, skips = { short } }, + failures_all = vim.deepcopy(failures), + } + end return nil - end - local name = data.Test - if not name or name == "" then - return nil - end - if data.Action == "pass" and not seen_pass[name] then - seen_pass[name] = true - table.insert(passes, name) - return { - passes = { name }, - failures = {}, - skips = {}, - failures_all = vim.deepcopy(failures), - } - elseif data.Action == "fail" and not seen_fail[name] then - seen_fail[name] = true - table.insert(failures, name) - return { - passes = {}, - failures = { name }, - skips = {}, - failures_all = vim.deepcopy(failures), - } - elseif data.Action == "skip" and not seen_pass[name] then - seen_pass[name] = true - table.insert(skips, name) - return { - passes = {}, - failures = {}, - skips = { name }, - failures_all = vim.deepcopy(failures), - } - end - return nil - end, + end, on_complete = function(_output, _state) return nil end, diff --git a/lua/test-samurai/runners/js-jest.lua b/lua/test-samurai/runners/js-jest.lua index 6959ba2..b26a3f6 100644 --- a/lua/test-samurai/runners/js-jest.lua +++ b/lua/test-samurai/runners/js-jest.lua @@ -4,5 +4,5 @@ return js.new({ name = "js-jest", framework = "jest", command = { "npx", "jest" }, - json_args = { "--json" }, + json_args = { "--json", "--verbose" }, }) diff --git a/lua/test-samurai/runners/js.lua b/lua/test-samurai/runners/js.lua index f3c51f6..20769f8 100644 --- a/lua/test-samurai/runners/js.lua +++ b/lua/test-samurai/runners/js.lua @@ -418,28 +418,123 @@ function M.new(opts) } end - local function parse_jest_like(output) - local ok, data = pcall(vim.json.decode, output) - if not ok or type(data) ~= "table" then + local function parse_jest_results(data) + if type(data) ~= "table" then return nil end local passes = {} local failures = {} local skips = {} + local display = { passes = {}, failures = {}, skips = {} } for _, result in ipairs(data.testResults or {}) do for _, assertion in ipairs(result.assertionResults or {}) do - local title = assertion.fullName or assertion.title - if assertion.status == "passed" and title then - table.insert(passes, title) - elseif assertion.status == "failed" and title then - table.insert(failures, title) + local full = assertion.fullName or assertion.title + local short = assertion.title or assertion.fullName + if assertion.status == "passed" and full then + table.insert(passes, full) + if short then + table.insert(display.passes, short) + end + elseif assertion.status == "failed" and full then + table.insert(failures, full) + if short then + table.insert(display.failures, short) + end elseif (assertion.status == "pending" or assertion.status == "skipped" or assertion.status == "todo") - and title then - table.insert(skips, title) + and full then + table.insert(skips, full) + if short then + table.insert(display.skips, short) + end end end end - return { passes = passes, failures = failures, skips = skips } + return { passes = passes, failures = failures, skips = skips, display = display } + end + + local function parse_jest_like(output) + local ok, data = pcall(vim.json.decode, output) + if ok and type(data) == "table" then + return parse_jest_results(data) + end + if not output or output == "" then + return nil + end + local start_idx = output:find("{", 1, true) + if not start_idx then + return nil + end + local end_idx = nil + for i = #output, start_idx, -1 do + if output:sub(i, i) == "}" then + end_idx = i + break + end + end + if not end_idx then + return nil + end + local candidate = output:sub(start_idx, end_idx) + local ok2, data2 = pcall(vim.json.decode, candidate) + if not ok2 or type(data2) ~= "table" then + return nil + end + return parse_jest_results(data2) + end + + local jest_symbols = { + pass = string.char(0xE2, 0x9C, 0x93), + fail = string.char(0xE2, 0x9C, 0x95), + skip = string.char(0xE2, 0x97, 0x8B), + } + + local function shorten_js_name(name) + if not name or name == "" then + return nil + end + local last = name:match(".*>%s*([^>]+)$") + if last then + return last:gsub("^%s+", ""):gsub("%s+$", "") + end + last = name:match(".*›%s*([^›]+)$") + if last then + return last:gsub("^%s+", ""):gsub("%s+$", "") + end + last = name:match(".*»%s*([^»]+)$") + if last then + return last:gsub("^%s+", ""):gsub("%s+$", "") + end + return name + end + + local function trim_jest_verbose_name(name) + if not name then + return nil + end + name = name:gsub("%s+%(%d+%.?%d*%s*ms%)%s*$", "") + name = name:gsub("%s+%(%d+%.?%d*%s*sec%)%s*$", "") + name = name:gsub("%s+%b()%s*$", "") + name = name:gsub("%s+$", "") + return name + end + + local function parse_jest_verbose_line(line) + if not line or line == "" then + return nil, nil + end + local name = line:match("^%s*" .. jest_symbols.pass .. "%s+(.+)$") + if name then + return "pass", trim_jest_verbose_name(name) + end + name = line:match("^%s*" .. jest_symbols.fail .. "%s+(.+)$") + if name then + return "fail", trim_jest_verbose_name(name) + end + name = line:match("^%s*" .. jest_symbols.skip .. "%s+(.+)$") + if name then + return "skip", trim_jest_verbose_name(name) + end + return nil, nil end local function parse_mocha(output) @@ -448,6 +543,7 @@ function M.new(opts) local passes = {} local failures = {} local skips = {} + local display = { passes = {}, failures = {}, 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 @@ -456,56 +552,89 @@ function M.new(opts) 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) + local full = payload.fullTitle or payload.title + local short = payload.title or payload.fullTitle + if event == "pass" and full then + table.insert(passes, full) + if short then + table.insert(display.passes, short) + end + elseif event == "fail" and full then + table.insert(failures, full) + if short then + table.insert(display.failures, short) + end + elseif event == "pending" and full then + table.insert(skips, full) + if short then + table.insert(display.skips, short) + end end end end if #passes == 0 and #failures == 0 and #skips == 0 then return nil end - return { passes = passes, failures = failures, skips = skips } + return { passes = passes, failures = failures, skips = skips, display = display } end local passes = {} local failures = {} local skips = {} + local display = { passes = {}, failures = {}, skips = {} } if type(data.tests) == "table" then for _, test in ipairs(data.tests) do - local title = test.fullTitle or test.title - if test.state == "passed" and title then - table.insert(passes, title) - elseif test.state == "failed" and title then - table.insert(failures, title) - elseif test.state == "pending" and title then - table.insert(skips, title) + local full = test.fullTitle or test.title + local short = test.title or test.fullTitle + if test.state == "passed" and full then + table.insert(passes, full) + if short then + table.insert(display.passes, short) + end + elseif test.state == "failed" and full then + table.insert(failures, full) + if short then + table.insert(display.failures, short) + end + elseif test.state == "pending" and full then + table.insert(skips, full) + if short then + table.insert(display.skips, short) + end end end elseif type(data.passes) == "table" or type(data.failures) == "table" then for _, test in ipairs(data.passes or {}) do - local title = test.fullTitle or test.title - if title then - table.insert(passes, title) + local full = test.fullTitle or test.title + local short = test.title or test.fullTitle + if full then + table.insert(passes, full) + if short then + table.insert(display.passes, short) + end end end for _, test in ipairs(data.failures or {}) do - local title = test.fullTitle or test.title - if title then - table.insert(failures, title) + local full = test.fullTitle or test.title + local short = test.title or test.fullTitle + if full then + table.insert(failures, full) + if short then + table.insert(display.failures, short) + end end end for _, test in ipairs(data.pending or {}) do - local title = test.fullTitle or test.title - if title then - table.insert(skips, title) + local full = test.fullTitle or test.title + local short = test.title or test.fullTitle + if full then + table.insert(skips, full) + if short then + table.insert(display.skips, short) + end end end end - return { passes = passes, failures = failures, skips = skips } + return { passes = passes, failures = failures, skips = skips, display = display } end local function parse_output(output) @@ -523,11 +652,137 @@ function M.new(opts) local state = { raw = {}, done = false, saw_stream = false } local failures = {} local skips = {} + local jest_seen_pass = {} + local jest_seen_fail = {} + local jest_seen_skip = {} + local jest_json_collecting = false + local jest_json_buffer = {} + local jest_streamed = false + + local function reset_list(dst, src) + for i = #dst, 1, -1 do + dst[i] = nil + end + for _, item in ipairs(src or {}) do + if item and item ~= "" then + table.insert(dst, item) + end + end + end + + local function emit_jest_results(results, emit_output) + if not results then + return nil + end + local display = results.display or {} + if emit_output == false then + reset_list(failures, results.failures or {}) + return { + passes = {}, + failures = {}, + skips = {}, + failures_all = vim.deepcopy(failures), + } + end + local out = { + passes = {}, + failures = {}, + skips = {}, + failures_all = {}, + display = { passes = {}, failures = {}, skips = {} }, + } + for i, title in ipairs(results.passes or {}) do + if title and not jest_seen_pass[title] then + jest_seen_pass[title] = true + table.insert(out.passes, title) + local show = display.passes and display.passes[i] or title + table.insert(out.display.passes, show) + end + end + for i, title in ipairs(results.failures or {}) do + if title and not jest_seen_fail[title] then + jest_seen_fail[title] = true + table.insert(failures, title) + table.insert(out.failures, title) + local show = display.failures and display.failures[i] or title + table.insert(out.display.failures, show) + end + end + for i, title in ipairs(results.skips or {}) do + if title and not jest_seen_skip[title] then + jest_seen_skip[title] = true + table.insert(out.skips, title) + local show = display.skips and display.skips[i] or title + table.insert(out.display.skips, show) + end + end + out.failures_all = vim.deepcopy(failures) + if #out.passes == 0 and #out.failures == 0 and #out.skips == 0 then + return nil + end + return out + end + + local function feed_jest_json(line) + if not jest_json_collecting then + if not line:match("^%s*{") then + return nil + end + jest_json_collecting = true + jest_json_buffer = { line } + else + table.insert(jest_json_buffer, line) + end + local candidate = table.concat(jest_json_buffer, "\n") + local ok_buf, data_buf = pcall(vim.json.decode, candidate) + if ok_buf and type(data_buf) == "table" then + jest_json_collecting = false + jest_json_buffer = {} + return data_buf + end + return nil + end + + local function handle_jest_json(data) + local results = parse_jest_results(data) + if not results then + return nil + end + state.done = true + state.saw_stream = true + return emit_jest_results(results, not jest_streamed) + end return { on_line = function(line, _state) if state.done then return nil end + if runner.framework == "jest" then + local kind, name = parse_jest_verbose_line(line) + if kind and name then + jest_streamed = true + local results = { passes = {}, failures = {}, skips = {} } + if kind == "pass" then + results.passes = { name } + elseif kind == "fail" then + results.failures = { name } + elseif kind == "skip" then + results.skips = { name } + end + results.display = { + passes = kind == "pass" and { name } or {}, + failures = kind == "fail" and { name } or {}, + skips = kind == "skip" and { name } or {}, + } + return emit_jest_results(results, true) + end + + local data = feed_jest_json(line) + if data then + return handle_jest_json(data) + end + return nil + end if runner.framework == "mocha" and runner.json_args then local uses_stream = false for i = 1, #runner.json_args - 1 do @@ -551,6 +806,11 @@ function M.new(opts) passes = { payload.fullTitle }, failures = {}, skips = {}, + display = { + passes = { payload.title or payload.fullTitle }, + failures = {}, + skips = {}, + }, failures_all = vim.deepcopy(failures), } elseif event == "fail" and payload.fullTitle then @@ -560,6 +820,11 @@ function M.new(opts) passes = {}, failures = { payload.fullTitle }, skips = {}, + display = { + passes = {}, + failures = { payload.title or payload.fullTitle }, + skips = {}, + }, failures_all = vim.deepcopy(failures), } elseif event == "pending" and payload.fullTitle then @@ -569,6 +834,11 @@ function M.new(opts) passes = {}, failures = {}, skips = { payload.fullTitle }, + display = { + passes = {}, + failures = {}, + skips = { payload.title or payload.fullTitle }, + }, failures_all = vim.deepcopy(failures), } elseif event == "start" or event == "end" then @@ -599,29 +869,48 @@ function M.new(opts) if skip_title then skip_title = skip_title:gsub("%s+$", "") table.insert(skips, skip_title) + state.saw_stream = true return { passes = {}, failures = {}, skips = { skip_title }, + display = { + passes = {}, + failures = {}, + skips = { shorten_js_name(skip_title) or skip_title }, + }, failures_all = vim.deepcopy(failures), } end ok_title = ok_title:gsub("%s+#%s+time=.*$", "") + state.saw_stream = true return { passes = { ok_title }, failures = {}, skips = {}, + display = { + passes = { shorten_js_name(ok_title) or ok_title }, + failures = {}, + skips = {}, + }, failures_all = vim.deepcopy(failures), } end local fail_title = line:match("^not ok%s+%d+%s+%-%s+(.+)") if fail_title then fail_title = fail_title:gsub("%s+#%s+time=.*$", "") + local display_name = shorten_js_name(fail_title) table.insert(failures, fail_title) + state.saw_stream = true return { passes = {}, failures = { fail_title }, skips = {}, + display = { + passes = {}, + failures = { display_name or fail_title }, + skips = {}, + }, failures_all = vim.deepcopy(failures), } end @@ -637,6 +926,18 @@ function M.new(opts) return results end, on_complete = function(output, _state) + if runner.framework == "jest" then + if state.done then + return nil + end + local results = parse_jest_like(output) + if results then + state.done = true + state.saw_stream = true + return emit_jest_results(results, not jest_streamed) + end + return nil + end if state.done or state.saw_stream then return nil end diff --git a/tests/test_samurai_failed_only_spec.lua b/tests/test_samurai_failed_only_spec.lua index cabb9af..a9fc521 100644 --- a/tests/test_samurai_failed_only_spec.lua +++ b/tests/test_samurai_failed_only_spec.lua @@ -91,11 +91,11 @@ describe("TSamFailedOnly", function() assert.equals(2, #calls) assert.are.same( - { "npx", "jest", "--json", "/tmp/project/foo_failed_only.test.ts", "-t", "inner 2" }, + { "npx", "jest", "--json", "--verbose", "/tmp/project/foo_failed_only.test.ts", "-t", "inner 2" }, calls[1].cmd ) assert.are.same( - { "npx", "jest", "--json", "-t", "outer inner 2", "/tmp/project/foo_failed_only.test.ts" }, + { "npx", "jest", "--json", "--verbose", "-t", "outer inner 2", "/tmp/project/foo_failed_only.test.ts" }, calls[2].cmd ) end) @@ -332,7 +332,7 @@ describe("TSamFailedOnly", function() assert.equals(3, #calls) assert.are.same(calls[1].cmd, calls[3].cmd) assert.are.same( - { "npx", "jest", "--json", "-t", "outer inner 2", "/tmp/project/foo_failed_only_last.test.ts" }, + { "npx", "jest", "--json", "--verbose", "-t", "outer inner 2", "/tmp/project/foo_failed_only_last.test.ts" }, calls[2].cmd ) end) diff --git a/tests/test_samurai_js_spec.lua b/tests/test_samurai_js_spec.lua index 14b5d4c..46ba3aa 100644 --- a/tests/test_samurai_js_spec.lua +++ b/tests/test_samurai_js_spec.lua @@ -52,7 +52,7 @@ describe("test-samurai js runner (jest)", function() assert.is_true(spec.cwd:match("tmp$") ~= nil) local cmd_spec = jest.build_command(spec) - assert.are.same({ "npx", "jest", "--json", spec.file, "-t", "inner 2" }, cmd_spec.cmd) + assert.are.same({ "npx", "jest", "--json", "--verbose", spec.file, "-t", "inner 2" }, cmd_spec.cmd) end) it("returns describe block when cursor is between it() calls", function() diff --git a/tests/test_samurai_last_spec.lua b/tests/test_samurai_last_spec.lua index 3d7afc5..87f4d50 100644 --- a/tests/test_samurai_last_spec.lua +++ b/tests/test_samurai_last_spec.lua @@ -130,7 +130,7 @@ describe("TSamLast", function() assert.equals(2, #calls) assert.are.same( - { "npx", "jest", "--json", "/tmp/project/foo_last.test.ts", "-t", "inner 2" }, + { "npx", "jest", "--json", "--verbose", "/tmp/project/foo_last.test.ts", "-t", "inner 2" }, calls[1].cmd ) assert.are.same(calls[1].cmd, calls[2].cmd) diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index 881bc7d..68c675c 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -151,11 +151,11 @@ describe("test-samurai output formatting", function() local has_skip = false local has_fail = false for _, line in ipairs(lines) do - if line == "[ PASS ] - outer inner 1" then + if line == "[ PASS ] - inner 1" then has_pass = true - elseif line == "[ SKIP ] - outer inner skip" then + elseif line == "[ SKIP ] - inner skip" then has_skip = true - elseif line == "[ FAIL ] - outer inner 2" then + elseif line == "[ FAIL ] - inner 2" then has_fail = true end end @@ -218,6 +218,186 @@ describe("test-samurai output formatting", function() assert.is_false(has_raw_json) end) + it("formats multi-line jest JSON output and ignores non-JSON lines", function() + local lines_out = { + "Some log line", + "{", + ' "testResults": [', + " {", + ' "assertionResults": [', + ' {"status":"passed","title":"inner 1","fullName":"outer inner 1"}', + " ]", + " }", + " ]", + "}", + } + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, lines_out, 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_multiline_json.test.ts") + vim.bo[bufnr].filetype = "typescript" + 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 ] - inner 1" then + has_pass = true + elseif line == ' "testResults": [' then + has_raw_json = true + end + end + assert.is_true(has_pass) + assert.is_false(has_raw_json) + end) + + it("formats jest verbose output as PASS/FAIL/SKIP lines", function() + local check = string.char(0xE2, 0x9C, 0x93) + local cross = string.char(0xE2, 0x9C, 0x95) + local circle = string.char(0xE2, 0x97, 0x8B) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { + " PASS /tmp/output_verbose.test.ts", + " " .. check .. " inner 1 (5 ms)", + " " .. circle .. " inner skip (skipped)", + " " .. cross .. " inner 2 (1 ms)", + }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 1, nil) + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_verbose.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 7, 0 }) + + core.run_all() + + 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_skip = false + local has_fail = false + local has_raw_verbose = false + for _, line in ipairs(lines) do + if line == "[ PASS ] - inner 1" then + has_pass = true + elseif line == "[ SKIP ] - inner skip" then + has_skip = true + elseif line == "[ FAIL ] - inner 2" then + has_fail = true + elseif line:match("^%s*PASS%s+") then + has_raw_verbose = true + end + end + assert.is_true(has_pass) + assert.is_true(has_skip) + assert.is_true(has_fail) + assert.is_false(has_raw_verbose) + end) + + it("formats go subtests as short names", function() + local json_line = vim.json.encode({ + Action = "pass", + Test = "TestHandleGet/returns_200", + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json_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_go_short_test.go") + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package main", + "import \"testing\"", + "", + "func TestHandleGet(t *testing.T) {", + " t.Run(\"returns_200\", func(t *testing.T) {", + " -- inside test", + " })", + "}", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 6, 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 ] - returns_200" then + has_pass = true + elseif line == json_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 output for mocha json-stream", function() test_samurai.setup({ runner_modules = { @@ -323,7 +503,7 @@ describe("test-samurai output formatting", function() local has_pass = false local has_raw_json = false for _, line in ipairs(lines) do - if line == "[ PASS ] - API :: /brands... GET: /" then + if line == "[ PASS ] - GET: /" then has_pass = true elseif line == pass_line then has_raw_json = true @@ -419,10 +599,12 @@ describe("test-samurai output formatting", function() local pass_line = vim.json.encode({ event = "pass", + title = "inner 1", fullTitle = "outer inner 1", }) local fail_line = vim.json.encode({ event = "fail", + title = "inner 2", fullTitle = "outer inner 2", }) @@ -475,9 +657,9 @@ describe("test-samurai output formatting", function() local has_pass = false local has_fail = false for _, line in ipairs(lines) do - if line == "[ PASS ] - outer inner 1" then + if line == "[ PASS ] - inner 1" then has_pass = true - elseif line == "[ FAIL ] - outer inner 2" then + elseif line == "[ FAIL ] - inner 2" then has_fail = true end end @@ -516,9 +698,9 @@ describe("test-samurai output formatting", function() opts.on_stdout(1, { "TAP version 13", "1..2", - "ok 1 - outer inner 1 # time=1.00ms", - "ok 2 - outer inner skip # SKIP not now", - "not ok 2 - outer inner 2 # time=2.00ms", + "ok 1 - outer > inner 1 # time=1.00ms", + "ok 2 - outer > inner skip # SKIP not now", + "not ok 2 - outer > inner 2 # time=2.00ms", }, nil) end if opts and opts.on_exit then @@ -556,11 +738,11 @@ describe("test-samurai output formatting", function() local has_skip = false local has_fail = false for _, line in ipairs(lines) do - if line == "[ PASS ] - outer inner 1" then + if line == "[ PASS ] - inner 1" then has_pass = true - elseif line == "[ SKIP ] - outer inner skip" then + elseif line == "[ SKIP ] - inner skip" then has_skip = true - elseif line == "[ FAIL ] - outer inner 2" then + elseif line == "[ FAIL ] - inner 2" then has_fail = true end end