From 279fc56ee5899ab996ef18a761b718b224347bd8 Mon Sep 17 00:00:00 2001 From: "M.Schirmer" Date: Fri, 12 Jun 2026 12:13:29 +0200 Subject: [PATCH] fix false detecting skipped tests --- lua/test-samurai-vitest-runner/init.lua | 84 +++++++++++------- ...er.js => test_samurai_vitest_reporter.mjs} | 33 ++++--- tests/test_vitest_runner_spec.lua | 86 ++++++++++++++++++- 3 files changed, 156 insertions(+), 47 deletions(-) rename reporter/{test_samurai_vitest_reporter.js => test_samurai_vitest_reporter.mjs} (72%) diff --git a/lua/test-samurai-vitest-runner/init.lua b/lua/test-samurai-vitest-runner/init.lua index f6c7ce4..10140c6 100644 --- a/lua/test-samurai-vitest-runner/init.lua +++ b/lua/test-samurai-vitest-runner/init.lua @@ -268,7 +268,30 @@ local function reporter_path() source = source:sub(2) end local dir = vim.fs.dirname(source) - return vim.fs.normalize(dir .. "/../../reporter/test_samurai_vitest_reporter.js") + return vim.fs.normalize(dir .. "/../../reporter/test_samurai_vitest_reporter.mjs") +end + +local function vitest_bin(cwd) + if cwd and cwd ~= "" then + local bin = vim.fs.normalize(cwd .. "/node_modules/.bin/vitest") + if vim.fn.executable(bin) == 1 then + return { bin } + end + end + return { "npx", "vitest" } +end + +local function find_vitest_config(cwd) + if not cwd or cwd == "" then + return nil + end + for _, name in ipairs({ "vitest.config.ts", "vitest.config.js" }) do + local candidate = cwd .. "/" .. name + if vim.fn.filereadable(candidate) == 1 then + return candidate + end + end + return nil end local function find_vitest_setup(cwd) @@ -298,17 +321,15 @@ local VITEST_CONFIG_MARKERS = { } local function base_cmd(cwd) - local cmd = { - "npx", - "vitest", - "run", - "--reporter", - reporter_path(), - } + local bin = vitest_bin(cwd) + local cmd = vim.list_extend(vim.deepcopy(bin), { "run", "--reporter", reporter_path() }) + local cfg = find_vitest_config(cwd) + if cfg then + vim.list_extend(cmd, { "--config", cfg }) + end local setup = find_vitest_setup(cwd) if setup then - table.insert(cmd, "--setupFiles") - table.insert(cmd, setup) + vim.list_extend(cmd, { "--setupFiles", setup }) end return cmd end @@ -584,35 +605,34 @@ function runner.parse_results(output) if not output or output == "" then return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } } end + local results_by_name = {} + for line in output:gmatch("[^\n]+") do + local data = parse_result_line(line) + if data and data.name and data.status then + results_by_name[data.name] = data + end + end + local passes = {} local failures = {} local skips = {} local pass_display = {} local fail_display = {} local skip_display = {} - local seen = { - passes = {}, - failures = {}, - skips = {}, - } - for line in output:gmatch("[^\n]+") do - local data = parse_result_line(line) - if data and data.name and data.status then - local kind = STATUS_MAP[data.status] - if kind and not seen[kind][data.name] then - seen[kind][data.name] = true - if kind == "passes" then - table.insert(passes, data.name) - pass_display[data.name] = data.name - elseif kind == "failures" then - table.insert(failures, data.name) - fail_display[data.name] = data.name - else - table.insert(skips, data.name) - skip_display[data.name] = data.name - end - update_location_cache(data.name, data) + for name, data in pairs(results_by_name) do + local kind = STATUS_MAP[data.status] + if kind then + if kind == "passes" then + table.insert(passes, name) + pass_display[name] = name + elseif kind == "failures" then + table.insert(failures, name) + fail_display[name] = name + else + table.insert(skips, name) + skip_display[name] = name end + update_location_cache(name, data) end end diff --git a/reporter/test_samurai_vitest_reporter.js b/reporter/test_samurai_vitest_reporter.mjs similarity index 72% rename from reporter/test_samurai_vitest_reporter.js rename to reporter/test_samurai_vitest_reporter.mjs index 9eea45e..32e172c 100644 --- a/reporter/test_samurai_vitest_reporter.js +++ b/reporter/test_samurai_vitest_reporter.mjs @@ -1,5 +1,5 @@ /** - * test_samurai_vitest_reporter.js + * test_samurai_vitest_reporter.mjs * * Custom Vitest 2.x reporter for test-samurai. * @@ -21,8 +21,10 @@ const RESULT_PREFIX = 'TSAMURAI_RESULT '; export default class TestSamuraiVitestReporter { onTestCaseResult(testCase) { const name = buildListingName(testCase); - const status = mapStatus(testCase.result?.state); - const output = buildOutput(testCase); + const status = mapStatus(testCase.task); + if (!status) return; + + const output = buildOutput(testCase.task); const payload = { name, @@ -52,24 +54,27 @@ function buildListingName(testCase) { } /** - * Maps Vitest result states to test-samurai status strings. + * Maps Vitest task result states to test-samurai status strings. + * Falls back to task.mode if result.state is missing. */ -function mapStatus(state) { - const map = { - pass: 'passed', - fail: 'failed', - skip: 'skipped', - todo: 'skipped', - }; - return map[state] ?? 'skipped'; +function mapStatus(task) { + if (!task) return null; + const state = task.result?.state; + if (state === 'pass') return 'passed'; + if (state === 'fail') return 'failed'; + if (state === 'skip') return 'skipped'; + const mode = task.mode; + if (mode === 'skip' || mode === 'todo') return 'skipped'; + return null; } /** * Collects error messages and stack traces for failed tests. */ -function buildOutput(testCase) { +function buildOutput(task) { const lines = []; - const errors = testCase.result?.errors ?? []; + if (!task) return lines; + const errors = task.result?.errors ?? []; for (const err of errors) { if (typeof err.message === 'string' && err.message.length > 0) { err.message.split('\n').forEach((line) => { diff --git a/tests/test_vitest_runner_spec.lua b/tests/test_vitest_runner_spec.lua index 4381b19..ffd3c39 100644 --- a/tests/test_vitest_runner_spec.lua +++ b/tests/test_vitest_runner_spec.lua @@ -38,7 +38,7 @@ local function get_reporter_path() source = source:sub(2) end local dir = vim.fs.dirname(source) - return vim.fs.normalize(dir .. "/../../reporter/test_samurai_vitest_reporter.js") + return vim.fs.normalize(dir .. "/../../reporter/test_samurai_vitest_reporter.mjs") end describe("test-samurai-vitest-runner", function() @@ -539,4 +539,88 @@ describe("test-samurai-vitest-runner", function() local result = parser.on_line("some random vitest output line", state) assert.is_nil(result) end) + + it("parse_results deduplicates across buckets (first skipped, then passed)", function() + local output = "TSAMURAI_RESULT " .. vim.json.encode({ + name = "footer/evergreen", + status = "skipped", + file = "/tmp/footer.test.ts", + location = { line = 5, column = 1 }, + }) .. "\n" + .. "TSAMURAI_RESULT " .. vim.json.encode({ + name = "footer/evergreen", + status = "passed", + file = "/tmp/footer.test.ts", + location = { line = 5, column = 1 }, + }) + + local results = runner.parse_results(output) + assert.equals(1, #results.passes) + assert.equals(0, #results.skips) + assert.equals("footer/evergreen", results.passes[1]) + end) + + it("build_command uses local node_modules/.bin/vitest if available", function() + with_project(VITEST_PACKAGE, function(root) + local bin_dir = root .. "/node_modules/.bin" + vim.fn.mkdir(bin_dir, "p") + write_file(bin_dir .. "/vitest", "#!/bin/sh\necho mock") + vim.fn.setfperm(bin_dir .. "/vitest", "rwxr-xr-x") + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, root .. "/example.test.ts") + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("footer", function()', + ' it("evergreen", function()', + ' assert.true(true)', + ' end)', + 'end)', + }) + + local spec = runner.find_nearest(bufnr, 2, 0) + local cmd = runner.build_command(spec).cmd + assert.equals(root .. "/node_modules/.bin/vitest", cmd[1]) + end) + end) + + it("base_cmd includes --config for vitest.config.ts", function() + with_project(VITEST_PACKAGE, function(root) + write_file(root .. "/vitest.config.ts", "export default { }") + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, root .. "/example.test.ts") + + local cmd = runner.build_file_command(bufnr).cmd + local has_config = false + for i, arg in ipairs(cmd) do + if arg == "--config" and cmd[i + 1] and cmd[i + 1]:match("vitest%.config%.ts$") then + has_config = true + break + end + end + assert.is_true(has_config) + end) + end) + + it("reporter_path() returns .mjs file", function() + local path = get_reporter_path() + assert.is_true(path:match("test_samurai_vitest_reporter%.mjs$") ~= nil) + end) + + it("build_file_command includes .mjs reporter path", function() + with_project(VITEST_PACKAGE, function(root) + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, root .. "/example.test.ts") + + local cmd = runner.build_file_command(bufnr).cmd + local has_mjs_reporter = false + for _, arg in ipairs(cmd) do + if arg:match("test_samurai_vitest_reporter%.mjs$") then + has_mjs_reporter = true + break + end + end + assert.is_true(has_mjs_reporter) + end) + end) end)