add TSamFailedOnly command and change test output within the floating window

This commit is contained in:
2025-12-25 16:40:50 +01:00
parent cbc3e201ae
commit 1e2e881acd
17 changed files with 1074 additions and 448 deletions

View File

@@ -0,0 +1,15 @@
describe("test-samurai commands", function()
before_each(function()
vim.g.loaded_test_samurai_plugin = nil
vim.cmd("runtime plugin/test-samurai.lua")
end)
it("registers TSamFailedOnly command", function()
assert.equals(2, vim.fn.exists(":TSamFailedOnly"))
end)
it("registers <leader>te keymap", function()
local map = vim.fn.maparg("<leader>te", "n")
assert.is_true(map ~= nil and map ~= "")
end)
end)

View File

@@ -0,0 +1,225 @@
local test_samurai = require("test-samurai")
local core = require("test-samurai.core")
local function mkbuf(path, ft, lines)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = ft
if lines then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end
return bufnr
end
local function stub_jobstart(opts_config)
local calls = {}
local orig = vim.fn.jobstart
local idx = 0
local config = opts_config or {}
vim.fn.jobstart = function(cmd, opts)
idx = idx + 1
table.insert(calls, { cmd = cmd, opts = opts })
local code = 0
if type(config.exit_codes) == "table" then
code = config.exit_codes[idx] or 0
elseif type(config.exit_codes) == "number" then
code = config.exit_codes
end
local out = config.stdout and config.stdout[idx] or nil
if out and opts and opts.on_stdout then
if type(out) == "string" then
out = { out }
end
opts.on_stdout(1, out, nil)
end
local err = config.stderr and config.stderr[idx] or nil
if err and opts and opts.on_stderr then
if type(err) == "string" then
err = { err }
end
opts.on_stderr(1, err, nil)
end
if opts and opts.on_exit then
opts.on_exit(1, code, nil)
end
return 1
end
return calls, orig
end
describe("TSamFailedOnly", function()
before_each(function()
test_samurai.setup()
end)
it("reruns failed jest tests with --onlyFailures", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "passed", title = "inner 1", fullName = "outer inner 1" },
{ status = "failed", title = "inner 2", fullName = "outer inner 2" },
},
},
},
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 0 },
stdout = { { json } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only.test.ts", "typescript", {
'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_nearest()
core.run_failed_only()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same(
{ "npx", "jest", "--json", "/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" },
calls[2].cmd
)
end)
it("falls back to TSamLast when last run had no failures", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "passed", title = "inner 1", fullName = "outer inner 1" },
},
},
},
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 0, 0 },
stdout = { { json } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_pass.test.ts", "typescript", {
'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_nearest()
core.run_failed_only()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same(calls[1].cmd, calls[2].cmd)
end)
it("reruns failed go tests with -run regex", function()
local json_lines = {
vim.json.encode({ Action = "fail", Test = "TestFoo/first" }),
vim.json.encode({ Action = "fail", Test = "TestBar" }),
}
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 0 },
stdout = { json_lines },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_test.go", "go", {
"package main",
"import \"testing\"",
"",
"func TestFoo(t *testing.T) {",
" t.Run(\"first\", func(t *testing.T) {",
" -- inside first",
" })",
"}",
"",
"func TestBar(t *testing.T) {",
" -- inside bar",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
core.run_all()
core.run_failed_only()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same({ "go", "test", "-json", "./..." }, calls[1].cmd)
assert.are.same({ "go", "test", "-json", "./...", "-run", "^(TestFoo/first|TestBar)$" }, calls[2].cmd)
end)
it("does not affect TSamLast history", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "failed", title = "inner 2", fullName = "outer inner 2" },
},
},
},
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 1, 1 },
stdout = { { json } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_last.test.ts", "typescript", {
'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_nearest()
core.run_failed_only()
core.run_last()
vim.fn.jobstart = orig_jobstart
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" },
calls[2].cmd
)
end)
end)

View File

@@ -95,7 +95,7 @@ describe("test-samurai go runner", function()
local cmd_spec_sub = go_runner.build_command(spec_sub)
assert.are.same(
{ "go", "test", "-v", "./pkg", "-run", "^TestFoo/first$" },
{ "go", "test", "-json", "./pkg", "-run", "^TestFoo/first$" },
cmd_spec_sub.cmd
)
@@ -108,7 +108,7 @@ describe("test-samurai go runner", function()
local cmd_spec_func = go_runner.build_command(spec_func)
assert.are.same(
{ "go", "test", "-v", "./", "-run", "^TestFoo($|/)" },
{ "go", "test", "-json", "./", "-run", "^TestFoo($|/)" },
cmd_spec_func.cmd
)
end)

View File

@@ -1,5 +1,6 @@
local jest = require("test-samurai.runners.js-jest")
local mocha = require("test-samurai.runners.js-mocha")
local vitest = require("test-samurai.runners.js-vitest")
local util = require("test-samurai.util")
describe("test-samurai js runner (jest)", function()
@@ -51,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", spec.file, "-t", "inner 2" }, cmd_spec.cmd)
assert.are.same({ "npx", "jest", "--json", spec.file, "-t", "inner 2" }, cmd_spec.cmd)
end)
it("returns describe block when cursor is between it() calls", function()
@@ -133,7 +134,7 @@ describe("test-samurai js runner (mocha)", function()
local cmd_spec = mocha.build_command(spec)
assert.are.same(
{ "npx", "mocha", "--fgrep", "outer inner 2", spec.file },
{ "npx", "mocha", "--reporter", "json-stream", "--fgrep", "outer inner 2", spec.file },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
@@ -154,7 +155,47 @@ describe("test-samurai js runner (mocha)", function()
util.find_root = orig_find_root
assert.are.same(
{ "npx", "mocha", "test/**/*.test.js" },
{ "npx", "mocha", "--reporter", "json-stream", "test/**/*.test.js" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
end)
describe("test-samurai js runner (vitest)", function()
it("builds vitest command with tap-flat reporter", function()
local spec = {
file = "/tmp/project/test/foo_nearest.test.ts",
cwd = "/tmp/project",
test_name = "inner 2",
full_name = "outer inner 2",
}
local cmd_spec = vitest.build_command(spec)
assert.are.same(
{ "npx", "vitest", "--reporter", "tap-flat", spec.file, "-t", "inner 2" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
it("builds vitest all command with tap-flat reporter", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/test/foo_all.test.ts")
vim.bo[bufnr].filetype = "typescript"
local orig_find_root = util.find_root
util.find_root = function(path, markers)
return "/tmp/project"
end
local cmd_spec = vitest.build_all_command(bufnr)
util.find_root = orig_find_root
assert.are.same(
{ "npx", "vitest", "--reporter", "tap-flat" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)

View File

@@ -49,7 +49,7 @@ describe("TSamLast", function()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same({ "go", "test", "-v", "./", "-run", "^TestFoo/first$" }, calls[1].cmd)
assert.are.same({ "go", "test", "-json", "./", "-run", "^TestFoo/first$" }, calls[1].cmd)
assert.are.same(calls[1].cmd, calls[2].cmd)
assert.equals(calls[1].opts.cwd, calls[2].opts.cwd)
end)
@@ -79,48 +79,10 @@ describe("TSamLast", function()
assert.equals(2, #calls)
assert.are.same(
{ "npx", "jest", "/tmp/project/foo_last.test.ts", "-t", "inner 2" },
{ "npx", "jest", "--json", "/tmp/project/foo_last.test.ts", "-t", "inner 2" },
calls[1].cmd
)
assert.are.same(calls[1].cmd, calls[2].cmd)
assert.equals(calls[1].opts.cwd, calls[2].opts.cwd)
end)
it("reruns last Lua command", function()
local calls, orig_jobstart = capture_jobstart()
local bufnr = mkbuf("/tmp/project/foo_last_spec.lua", "lua", {
"describe('outer', function()",
" it('inner 1', function()",
" local x = 1",
" end)",
"",
" it('inner 2', function()",
" local y = 2",
" end)",
"end)",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 7, 0 })
core.run_nearest()
core.run_last()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same({
"nvim",
"--headless",
"-u",
"/tmp/project/tests/minimal_init.lua",
"-c",
'PlenaryBustedFile /tmp/project/foo_last_spec.lua { busted_args = { "--filter", "inner 2" } }',
"-c",
"qa",
}, calls[1].cmd)
assert.are.same(calls[1].cmd, calls[2].cmd)
assert.equals(calls[1].opts.cwd, calls[2].opts.cwd)
end)
end)

View File

@@ -1,130 +0,0 @@
local test_samurai = require("test-samurai")
local lua_runner = require("test-samurai.runners.lua-plenary")
local util = require("test-samurai.util")
local function mkbuf(path, ft, lines)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = ft
if lines then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end
return bufnr
end
describe("test-samurai lua runner (plenary)", function()
it("detects lua spec files by suffix", function()
local bufnr = mkbuf("/tmp/test_samurai_lua_spec_unique_1_spec.lua", "lua")
assert.is_true(lua_runner.is_test_file(bufnr))
end)
it("finds nearest it() when cursor is inside it block and builds filtered command", function()
local bufnr = mkbuf("/tmp/project/tests/test_samurai_lua_nearest_unique_2_spec.lua", "lua", {
"describe('outer', function()",
" it('inner 1', function()",
" local x = 1",
" end)",
"",
" it('inner 2', function()",
" local y = 2",
" end)",
"end)",
})
local orig_find_root = util.find_root
util.find_root = function()
return "/tmp/project"
end
local spec, err = lua_runner.find_nearest(bufnr, 6, 0)
util.find_root = orig_find_root
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("inner 2", spec.test_name)
assert.equals("outer inner 2", spec.full_name)
assert.equals("/tmp/project", spec.cwd)
local cmd_spec = lua_runner.build_command(spec)
assert.equals("/tmp/project", cmd_spec.cwd)
assert.are.same({
"nvim",
"--headless",
"-u",
"/tmp/project/tests/minimal_init.lua",
"-c",
'PlenaryBustedFile ' .. spec.file .. ' { busted_args = { "--filter", "inner 2" } }',
"-c",
"qa",
}, cmd_spec.cmd)
end)
it("returns describe block when cursor is between it() calls", function()
local bufnr = mkbuf("/tmp/project/tests/test_samurai_lua_between_unique_3_spec.lua", "lua", {
"describe('outer', function()",
" it('inner 1', function()",
" local x = 1",
" end)",
"",
" it('inner 2', function()",
" local y = 2",
" end)",
"end)",
})
local orig_find_root = util.find_root
util.find_root = function()
return "/tmp/project"
end
local spec, err = lua_runner.find_nearest(bufnr, 4, 0)
util.find_root = orig_find_root
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("outer", spec.test_name)
assert.equals("outer", spec.full_name)
assert.equals("describe", spec.kind)
end)
it("builds all command via PlenaryBustedDirectory", function()
local bufnr = mkbuf("/tmp/project/tests/test_samurai_core_spec_unique_4_spec.lua", "lua")
local orig_find_root = util.find_root
util.find_root = function()
return "/tmp/project"
end
local cmd_spec = lua_runner.build_all_command(bufnr)
util.find_root = orig_find_root
assert.are.same({
"nvim",
"--headless",
"-u",
"/tmp/project/tests/minimal_init.lua",
"-c",
"PlenaryBustedDirectory tests",
"-c",
"qa",
}, cmd_spec.cmd)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
it("core selects lua runner for *_spec.lua buffers", function()
test_samurai.setup({
runner_modules = {
"test-samurai.runners.lua-plenary",
},
})
local bufnr = mkbuf("/tmp/project/tests/test_samurai_core_spec_unique_5_spec.lua", "lua")
local runner = require("test-samurai.core").get_runner_for_buf(bufnr)
assert.is_not_nil(runner)
assert.equals("lua-plenary", runner.name)
end)
end)

View File

@@ -76,4 +76,157 @@ describe("test-samurai public API", function()
assert.is_true(called)
end)
it("delegates test_failed_only to core.run_failed_only", function()
local called = false
local orig = core.run_failed_only
core.run_failed_only = function()
called = true
end
test_samurai.test_failed_only()
core.run_failed_only = orig
assert.is_true(called)
end)
end)
describe("test-samurai output formatting", function()
before_each(function()
test_samurai.setup()
end)
it("formats JSON output as PASS/FAIL lines", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "passed", title = "inner 1", fullName = "outer inner 1" },
{ status = "skipped", title = "inner skip", fullName = "outer inner skip" },
{ status = "failed", title = "inner 2", fullName = "outer inner 2" },
},
},
},
})
local orig_jobstart = vim.fn.jobstart
vim.fn.jobstart = function(_cmd, opts)
if opts and opts.on_stdout then
opts.on_stdout(1, { json }, 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_format.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_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_skip = false
local has_fail = false
for _, line in ipairs(lines) do
if line == "[ PASS ] - outer inner 1" then
has_pass = true
elseif line == "[ SKIP ] - outer inner skip" then
has_skip = true
elseif line == "[ FAIL ] - outer inner 2" then
has_fail = true
end
end
assert.is_true(has_pass)
assert.is_true(has_skip)
assert.is_true(has_fail)
end)
it("formats TAP output as PASS/FAIL lines", function()
test_samurai.setup({
runner_modules = {
"test-samurai.runners.js-vitest",
},
})
local orig_jobstart = vim.fn.jobstart
vim.fn.jobstart = function(_cmd, opts)
if opts and opts.on_stdout then
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",
}, 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_format.tap.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_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_skip = false
local has_fail = false
for _, line in ipairs(lines) do
if line == "[ PASS ] - outer inner 1" then
has_pass = true
elseif line == "[ SKIP ] - outer inner skip" then
has_skip = true
elseif line == "[ FAIL ] - outer inner 2" then
has_fail = true
end
end
assert.is_true(has_pass)
assert.is_true(has_skip)
assert.is_true(has_fail)
end)
end)