@@ -268,7 +268,30 @@ local function reporter_path()
|
|||||||
source = source:sub(2)
|
source = source:sub(2)
|
||||||
end
|
end
|
||||||
local dir = vim.fs.dirname(source)
|
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
|
end
|
||||||
|
|
||||||
local function find_vitest_setup(cwd)
|
local function find_vitest_setup(cwd)
|
||||||
@@ -298,17 +321,15 @@ local VITEST_CONFIG_MARKERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
local function base_cmd(cwd)
|
local function base_cmd(cwd)
|
||||||
local cmd = {
|
local bin = vitest_bin(cwd)
|
||||||
"npx",
|
local cmd = vim.list_extend(vim.deepcopy(bin), { "run", "--reporter", reporter_path() })
|
||||||
"vitest",
|
local cfg = find_vitest_config(cwd)
|
||||||
"run",
|
if cfg then
|
||||||
"--reporter",
|
vim.list_extend(cmd, { "--config", cfg })
|
||||||
reporter_path(),
|
end
|
||||||
}
|
|
||||||
local setup = find_vitest_setup(cwd)
|
local setup = find_vitest_setup(cwd)
|
||||||
if setup then
|
if setup then
|
||||||
table.insert(cmd, "--setupFiles")
|
vim.list_extend(cmd, { "--setupFiles", setup })
|
||||||
table.insert(cmd, setup)
|
|
||||||
end
|
end
|
||||||
return cmd
|
return cmd
|
||||||
end
|
end
|
||||||
@@ -584,35 +605,34 @@ function runner.parse_results(output)
|
|||||||
if not output or output == "" then
|
if not output or output == "" then
|
||||||
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
||||||
end
|
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 passes = {}
|
||||||
local failures = {}
|
local failures = {}
|
||||||
local skips = {}
|
local skips = {}
|
||||||
local pass_display = {}
|
local pass_display = {}
|
||||||
local fail_display = {}
|
local fail_display = {}
|
||||||
local skip_display = {}
|
local skip_display = {}
|
||||||
local seen = {
|
for name, data in pairs(results_by_name) do
|
||||||
passes = {},
|
local kind = STATUS_MAP[data.status]
|
||||||
failures = {},
|
if kind then
|
||||||
skips = {},
|
if kind == "passes" then
|
||||||
}
|
table.insert(passes, name)
|
||||||
for line in output:gmatch("[^\n]+") do
|
pass_display[name] = name
|
||||||
local data = parse_result_line(line)
|
elseif kind == "failures" then
|
||||||
if data and data.name and data.status then
|
table.insert(failures, name)
|
||||||
local kind = STATUS_MAP[data.status]
|
fail_display[name] = name
|
||||||
if kind and not seen[kind][data.name] then
|
else
|
||||||
seen[kind][data.name] = true
|
table.insert(skips, name)
|
||||||
if kind == "passes" then
|
skip_display[name] = name
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
update_location_cache(name, data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+19
-14
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* test_samurai_vitest_reporter.js
|
* test_samurai_vitest_reporter.mjs
|
||||||
*
|
*
|
||||||
* Custom Vitest 2.x reporter for test-samurai.
|
* Custom Vitest 2.x reporter for test-samurai.
|
||||||
*
|
*
|
||||||
@@ -21,8 +21,10 @@ const RESULT_PREFIX = 'TSAMURAI_RESULT ';
|
|||||||
export default class TestSamuraiVitestReporter {
|
export default class TestSamuraiVitestReporter {
|
||||||
onTestCaseResult(testCase) {
|
onTestCaseResult(testCase) {
|
||||||
const name = buildListingName(testCase);
|
const name = buildListingName(testCase);
|
||||||
const status = mapStatus(testCase.result?.state);
|
const status = mapStatus(testCase.task);
|
||||||
const output = buildOutput(testCase);
|
if (!status) return;
|
||||||
|
|
||||||
|
const output = buildOutput(testCase.task);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
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) {
|
function mapStatus(task) {
|
||||||
const map = {
|
if (!task) return null;
|
||||||
pass: 'passed',
|
const state = task.result?.state;
|
||||||
fail: 'failed',
|
if (state === 'pass') return 'passed';
|
||||||
skip: 'skipped',
|
if (state === 'fail') return 'failed';
|
||||||
todo: 'skipped',
|
if (state === 'skip') return 'skipped';
|
||||||
};
|
const mode = task.mode;
|
||||||
return map[state] ?? 'skipped';
|
if (mode === 'skip' || mode === 'todo') return 'skipped';
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects error messages and stack traces for failed tests.
|
* Collects error messages and stack traces for failed tests.
|
||||||
*/
|
*/
|
||||||
function buildOutput(testCase) {
|
function buildOutput(task) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const errors = testCase.result?.errors ?? [];
|
if (!task) return lines;
|
||||||
|
const errors = task.result?.errors ?? [];
|
||||||
for (const err of errors) {
|
for (const err of errors) {
|
||||||
if (typeof err.message === 'string' && err.message.length > 0) {
|
if (typeof err.message === 'string' && err.message.length > 0) {
|
||||||
err.message.split('\n').forEach((line) => {
|
err.message.split('\n').forEach((line) => {
|
||||||
@@ -38,7 +38,7 @@ local function get_reporter_path()
|
|||||||
source = source:sub(2)
|
source = source:sub(2)
|
||||||
end
|
end
|
||||||
local dir = vim.fs.dirname(source)
|
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
|
end
|
||||||
|
|
||||||
describe("test-samurai-vitest-runner", function()
|
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)
|
local result = parser.on_line("some random vitest output line", state)
|
||||||
assert.is_nil(result)
|
assert.is_nil(result)
|
||||||
end)
|
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)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user