543 lines
16 KiB
Lua
543 lines
16 KiB
Lua
local runner = require("test-samurai-vitest-runner")
|
|
|
|
local function write_file(path, content)
|
|
local dir = vim.fn.fnamemodify(path, ":h")
|
|
vim.fn.mkdir(dir, "p")
|
|
vim.fn.writefile(vim.split(content, "\n"), path)
|
|
end
|
|
|
|
local function with_project(package_json, fn)
|
|
local root = vim.fn.tempname()
|
|
vim.fn.mkdir(root, "p")
|
|
root = vim.loop.fs_realpath(root) or root
|
|
if package_json then
|
|
write_file(root .. "/package.json", package_json)
|
|
end
|
|
fn(root)
|
|
end
|
|
|
|
local VITEST_PACKAGE = [[
|
|
{
|
|
"devDependencies": {
|
|
"vitest": "^2.0.0"
|
|
}
|
|
}
|
|
]]
|
|
|
|
local NO_VITEST_PACKAGE = [[
|
|
{
|
|
"devDependencies": {
|
|
"jest": "^29.0.0"
|
|
}
|
|
}
|
|
]]
|
|
|
|
local function get_reporter_path()
|
|
local source = debug.getinfo(runner.build_command, "S").source
|
|
if source:sub(1, 1) == "@" then
|
|
source = source:sub(2)
|
|
end
|
|
local dir = vim.fs.dirname(source)
|
|
return vim.fs.normalize(dir .. "/../../reporter/test_samurai_vitest_reporter.js")
|
|
end
|
|
|
|
describe("test-samurai-vitest-runner", function()
|
|
it("detects Vitest test files by suffix and package.json", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr1, root .. "/example.test.ts")
|
|
assert.is_true(runner.is_test_file(bufnr1))
|
|
|
|
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr2, root .. "/example.spec.tsx")
|
|
assert.is_true(runner.is_test_file(bufnr2))
|
|
|
|
local bufnr3 = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr3, root .. "/example.ts")
|
|
assert.is_false(runner.is_test_file(bufnr3))
|
|
end)
|
|
end)
|
|
|
|
it("rejects test files when vitest dependency is missing", function()
|
|
with_project(NO_VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/example.test.ts")
|
|
assert.is_false(runner.is_test_file(bufnr))
|
|
end)
|
|
end)
|
|
|
|
it("detects .test.js, .spec.mjs, .test.cjs suffixes", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local cases = {
|
|
root .. "/a.test.js",
|
|
root .. "/b.spec.js",
|
|
root .. "/c.test.jsx",
|
|
root .. "/d.spec.tsx",
|
|
root .. "/e.test.mjs",
|
|
root .. "/f.spec.cjs",
|
|
}
|
|
for _, fname in ipairs(cases) do
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, fname)
|
|
assert.is_true(runner.is_test_file(bufnr), fname .. " should be detected")
|
|
end
|
|
end)
|
|
end)
|
|
|
|
it("finds nearest test with describe hierarchy", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/math.test.ts")
|
|
local lines = {
|
|
"describe('Math', () => {",
|
|
" test('adds', () => {",
|
|
" expect(1 + 1).toBe(2)",
|
|
" })",
|
|
"})",
|
|
}
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
|
|
local spec, err = runner.find_nearest(bufnr, 2, 0)
|
|
|
|
assert.is_nil(err)
|
|
assert.equals("adds", spec.test_name)
|
|
assert.equals("Math/adds", spec.full_name)
|
|
assert.are.same({ "Math", "adds" }, spec.vitest_parts)
|
|
assert.is_true(spec.file:match("math%.test%.ts$") ~= nil)
|
|
assert.equals(root, spec.cwd)
|
|
end)
|
|
end)
|
|
|
|
it("handles multiline describe and it declarations", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/multiline.test.ts")
|
|
local lines = {
|
|
"describe(",
|
|
" '<MyComponent/>',",
|
|
" () => {",
|
|
" describe(",
|
|
" 'renders properly...',",
|
|
" () => {",
|
|
" it(",
|
|
" 'the shadow root',",
|
|
" async () => {",
|
|
" expect(true).toBe(true)",
|
|
" }",
|
|
" )",
|
|
" }",
|
|
" )",
|
|
" }",
|
|
")",
|
|
}
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
|
|
local spec, err = runner.find_nearest(bufnr, 9, 0)
|
|
|
|
assert.is_nil(err)
|
|
assert.equals("the shadow root", spec.test_name)
|
|
assert.equals("<MyComponent/>/renders properly.../the shadow root", spec.full_name)
|
|
assert.are.same({ "<MyComponent/>", "renders properly...", "the shadow root" }, spec.vitest_parts)
|
|
end)
|
|
end)
|
|
|
|
it("uses describe block when cursor is between tests", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/between.test.ts")
|
|
local lines = {
|
|
"describe('<MyComponent/>', () => {",
|
|
" describe('renders properly...', () => {",
|
|
" it('the logo', async () => {",
|
|
" expect(true).toBe(true)",
|
|
" })",
|
|
" ",
|
|
" it('the shadow host', async () => {",
|
|
" expect(true).toBe(true)",
|
|
" })",
|
|
" })",
|
|
"})",
|
|
}
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
|
|
local spec, err = runner.find_nearest(bufnr, 5, 0)
|
|
|
|
assert.is_nil(err)
|
|
assert.equals("renders properly...", spec.test_name)
|
|
assert.equals("<MyComponent/>/renders properly...", spec.full_name)
|
|
assert.are.same({ "<MyComponent/>", "renders properly..." }, spec.vitest_parts)
|
|
assert.equals("describe", spec.kind)
|
|
end)
|
|
end)
|
|
|
|
it("falls back to file command when outside any describe", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/outside.test.ts")
|
|
local lines = {
|
|
"const value = 1",
|
|
"function helper() { return value }",
|
|
}
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
|
|
local spec, err = runner.find_nearest(bufnr, 1, 0)
|
|
assert.is_nil(err)
|
|
assert.equals("file", spec.kind)
|
|
|
|
local cmd_spec = runner.build_command(spec)
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
cmd_spec.cmd[#cmd_spec.cmd],
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.is_true(cmd_spec.cmd[#cmd_spec.cmd]:match("outside%.test%.ts$") ~= nil)
|
|
end)
|
|
end)
|
|
|
|
it("build_command uses npx vitest run with reporter and pattern", function()
|
|
local spec = {
|
|
file = "/tmp/math.test.ts",
|
|
cwd = "/tmp",
|
|
full_name = "Math/adds",
|
|
vitest_parts = { "Math", "adds" },
|
|
}
|
|
local cmd_spec = runner.build_command(spec)
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
"/tmp/math.test.ts",
|
|
"--testNamePattern",
|
|
"^.*adds$",
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.equals("/tmp", cmd_spec.cwd)
|
|
end)
|
|
|
|
it("build_command uses prefix pattern for describe blocks", function()
|
|
local spec = {
|
|
file = "/tmp/component.test.ts",
|
|
cwd = "/tmp",
|
|
kind = "describe",
|
|
vitest_parts = { "<MyComponent/>", "renders properly..." },
|
|
}
|
|
local cmd_spec = runner.build_command(spec)
|
|
local pattern = cmd_spec.cmd[#cmd_spec.cmd]
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
"/tmp/component.test.ts",
|
|
"--testNamePattern",
|
|
pattern,
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.is_true(pattern:find("MyComponent", 1, true) ~= nil)
|
|
assert.is_true(pattern:find("renders", 1, true) ~= nil)
|
|
assert.is_true(pattern:find("properly", 1, true) ~= nil)
|
|
assert.is_true(pattern:sub(-2) == ".*")
|
|
end)
|
|
|
|
it("build_file_command scopes to file", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/foo.test.ts")
|
|
|
|
local cmd_spec = runner.build_file_command(bufnr)
|
|
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
root .. "/foo.test.ts",
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.equals(root, cmd_spec.cwd)
|
|
end)
|
|
end)
|
|
|
|
it("adds vitest.setup.ts from project root to --setupFiles", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
write_file(root .. "/vitest.setup.ts", "// setup")
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/foo.test.ts")
|
|
|
|
local cmd_spec = runner.build_file_command(bufnr)
|
|
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
"--setupFiles",
|
|
root .. "/vitest.setup.ts",
|
|
root .. "/foo.test.ts",
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.equals(root, cmd_spec.cwd)
|
|
end)
|
|
end)
|
|
|
|
it("adds vitest.setup.js from test/.bin when root setup is missing", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
write_file(root .. "/test/.bin/vitest.setup.js", "// setup")
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/bar.test.ts")
|
|
|
|
local cmd_spec = runner.build_file_command(bufnr)
|
|
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
"--setupFiles",
|
|
root .. "/test/.bin/vitest.setup.js",
|
|
root .. "/bar.test.ts",
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.equals(root, cmd_spec.cwd)
|
|
end)
|
|
end)
|
|
|
|
it("build_all_command runs all project tests without file filter", function()
|
|
with_project(VITEST_PACKAGE, function(root)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_name(bufnr, root .. "/bar.test.ts")
|
|
|
|
local cmd_spec = runner.build_all_command(bufnr)
|
|
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.equals(root, cmd_spec.cwd)
|
|
end)
|
|
end)
|
|
|
|
it("build_failed_command narrows to failed tests", function()
|
|
local last_command = {
|
|
cmd = {
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
"/tmp/math.test.ts",
|
|
"--testNamePattern",
|
|
"^Old$",
|
|
},
|
|
cwd = "/tmp",
|
|
}
|
|
local failures = { "Math/adds", "edge (1+1)" }
|
|
|
|
local cmd_spec = runner.build_failed_command(last_command, failures, "file")
|
|
|
|
local pattern = cmd_spec.cmd[#cmd_spec.cmd]
|
|
assert.are.same(
|
|
{
|
|
"npx",
|
|
"vitest",
|
|
"run",
|
|
"--reporter",
|
|
get_reporter_path(),
|
|
"/tmp/math.test.ts",
|
|
"--testNamePattern",
|
|
pattern,
|
|
},
|
|
cmd_spec.cmd
|
|
)
|
|
assert.is_true(pattern:match("%^%..*adds%$") ~= nil)
|
|
assert.is_true(pattern:match("edge") ~= nil)
|
|
assert.is_true(pattern:find("\\(1", 1, true) ~= nil)
|
|
assert.is_true(pattern:find("\\+1", 1, true) ~= nil)
|
|
assert.equals("/tmp", cmd_spec.cwd)
|
|
end)
|
|
|
|
it("parse_results collects statuses and caches locations", function()
|
|
local output = table.concat({
|
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/adds",
|
|
status = "passed",
|
|
file = "/tmp/math.test.ts",
|
|
location = { line = 3, column = 4 },
|
|
}),
|
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/subtracts",
|
|
status = "failed",
|
|
file = "/tmp/math.test.ts",
|
|
location = { line = 10, column = 2 },
|
|
}),
|
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/skipped",
|
|
status = "skipped",
|
|
file = "/tmp/math.test.ts",
|
|
location = { line = 20, column = 2 },
|
|
}),
|
|
}, "\n")
|
|
|
|
local results = runner.parse_results(output)
|
|
assert.are.same({ "Math/adds" }, results.passes)
|
|
assert.are.same({ "Math/subtracts" }, results.failures)
|
|
assert.are.same({ "Math/skipped" }, results.skips)
|
|
assert.are.same({ "Math/adds" }, results.display.passes)
|
|
assert.are.same({ "Math/subtracts" }, results.display.failures)
|
|
assert.are.same({ "Math/skipped" }, results.display.skips)
|
|
end)
|
|
|
|
it("parse_results deduplicates repeated entries", function()
|
|
local line = "TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/adds",
|
|
status = "passed",
|
|
})
|
|
local results = runner.parse_results(line .. "\n" .. line)
|
|
assert.equals(1, #results.passes)
|
|
end)
|
|
|
|
it("output_parser streams per test case", function()
|
|
local parser = runner.output_parser()
|
|
local state = {}
|
|
local line = "TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/adds",
|
|
status = "failed",
|
|
file = "/tmp/math.test.ts",
|
|
location = { line = 3, column = 4 },
|
|
})
|
|
|
|
local results = parser.on_line(line, state)
|
|
|
|
assert.are.same({ "Math/adds" }, results.failures)
|
|
assert.are.same({ "Math/adds" }, results.display.failures)
|
|
assert.are.same({ "Math/adds" }, results.failures_all)
|
|
assert.is_nil(parser.on_complete("", state))
|
|
end)
|
|
|
|
it("keeps failures_all across non-failure lines", function()
|
|
local parser = runner.output_parser()
|
|
local state = {}
|
|
local fail_line = "TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/adds",
|
|
status = "failed",
|
|
})
|
|
local pass_line = "TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/other",
|
|
status = "passed",
|
|
})
|
|
|
|
parser.on_line(fail_line, state)
|
|
local results = parser.on_line(pass_line, state)
|
|
|
|
assert.are.same({ "Math/adds" }, results.failures_all)
|
|
end)
|
|
|
|
it("parse_test_output groups output lines per test", function()
|
|
local output = table.concat({
|
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "MyComponent/renders the slot",
|
|
status = "failed",
|
|
output = { "Expected: true", "Received: false" },
|
|
}),
|
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "MyComponent/renders the slot",
|
|
status = "failed",
|
|
output = { "at Object.<anonymous> (component.test.ts:5:3)" },
|
|
}),
|
|
}, "\n")
|
|
|
|
local results = runner.parse_test_output(output)
|
|
assert.are.same(
|
|
{ "Expected: true", "Received: false", "at Object.<anonymous> (component.test.ts:5:3)" },
|
|
results["MyComponent/renders the slot"]
|
|
)
|
|
end)
|
|
|
|
it("collect_failed_locations uses cached locations from parse_results", function()
|
|
local output = "TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "MyComponent/shadow DOM slot",
|
|
status = "failed",
|
|
file = "/tmp/component.test.ts",
|
|
location = { line = 8, column = 2 },
|
|
})
|
|
runner.parse_results(output)
|
|
|
|
local items = runner.collect_failed_locations({ "MyComponent/shadow DOM slot" }, nil, "file")
|
|
assert.equals(1, #items)
|
|
assert.equals("/tmp/component.test.ts", items[1].filename)
|
|
assert.equals(8, items[1].lnum)
|
|
assert.equals(2, items[1].col)
|
|
assert.equals("MyComponent/shadow DOM slot", items[1].text)
|
|
end)
|
|
|
|
it("collect_failed_locations uses cached locations from output_parser", function()
|
|
local parser = runner.output_parser()
|
|
local state = {}
|
|
parser.on_line("TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "MyComponent/adds slot",
|
|
status = "failed",
|
|
file = "/tmp/component.test.ts",
|
|
location = { line = 12, column = 5 },
|
|
}), state)
|
|
|
|
local items = runner.collect_failed_locations({ "MyComponent/adds slot" }, nil, "file")
|
|
assert.equals(1, #items)
|
|
assert.equals(12, items[1].lnum)
|
|
end)
|
|
|
|
it("collect_failed_locations deduplicates identical entries", function()
|
|
local output = "TSAMURAI_RESULT " .. vim.json.encode({
|
|
name = "Math/adds",
|
|
status = "failed",
|
|
file = "/tmp/math.test.ts",
|
|
location = { line = 3, column = 1 },
|
|
})
|
|
runner.parse_results(output)
|
|
|
|
local items = runner.collect_failed_locations({ "Math/adds", "Math/adds" }, nil, "file")
|
|
assert.equals(1, #items)
|
|
end)
|
|
|
|
it("returns empty results for empty output", function()
|
|
local results = runner.parse_results("")
|
|
assert.are.same({}, results.passes)
|
|
assert.are.same({}, results.failures)
|
|
assert.are.same({}, results.skips)
|
|
end)
|
|
|
|
it("output_parser ignores non-TSAMURAI lines", function()
|
|
local parser = runner.output_parser()
|
|
local state = {}
|
|
local result = parser.on_line("some random vitest output line", state)
|
|
assert.is_nil(result)
|
|
end)
|
|
end)
|