local runner = require("test-samurai-jest-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 JEST_PACKAGE = [[
{
"devDependencies": {
"jest": "^29.0.0"
}
}
]]
local NO_JEST_PACKAGE = [[
{
"devDependencies": {
"mocha": "^10.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_jest_reporter.js")
end
describe("test-samurai-jest-runner", function()
it("detects Jest test files by suffix and package.json", function()
with_project(JEST_PACKAGE, function(root)
local bufnr1 = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr1, root .. "/example.test.js")
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.js")
assert.is_false(runner.is_test_file(bufnr3))
end)
end)
it("rejects test files when jest dependency is missing", function()
with_project(NO_JEST_PACKAGE, function(root)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, root .. "/example.test.js")
assert.is_false(runner.is_test_file(bufnr))
end)
end)
it("finds nearest test with describe hierarchy", function()
with_project(JEST_PACKAGE, function(root)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, root .. "/math.test.js")
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.equals("Math adds", spec.jest_name)
assert.are.same({ "Math", "adds" }, spec.jest_parts)
assert.is_true(spec.file:match("math%.test%.js$") ~= nil)
assert.equals(root, spec.cwd)
end)
end)
it("handles multiline describe and it declarations", function()
with_project(JEST_PACKAGE, function(root)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, root .. "/multiline.test.js")
local lines = {
"describe(",
" '',",
" () => {",
" describe(",
" 'renders properly...',",
" () => {",
" it(",
" 'the teaser links',",
" 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 teaser links", spec.test_name)
assert.equals("/renders properly.../the teaser links", spec.full_name)
assert.are.same({ "", "renders properly...", "the teaser links" }, spec.jest_parts)
end)
end)
it("uses describe block when cursor is between tests", function()
with_project(JEST_PACKAGE, function(root)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, root .. "/between.test.js")
local lines = {
"describe('', () => {",
" describe('renders properly...', () => {",
" it('the logo', async () => {",
" expect(true).toBe(true)",
" })",
" ",
" it('the teaser links', 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("/renders properly...", spec.full_name)
assert.are.same({ "", "renders properly..." }, spec.jest_parts)
assert.equals("describe", spec.kind)
end)
end)
it("falls back to file command when outside any describe", function()
with_project(JEST_PACKAGE, function(root)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, root .. "/outside.test.js")
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",
"jest",
"--testLocationInResults",
"--reporters",
get_reporter_path(),
"--runTestsByPath",
cmd_spec.cmd[#cmd_spec.cmd],
},
cmd_spec.cmd
)
assert.is_true(cmd_spec.cmd[#cmd_spec.cmd]:match("outside%.test%.js$") ~= nil)
end)
end)
it("build_command uses npx jest with reporter and pattern", function()
local spec = {
file = "/tmp/math.test.js",
cwd = "/tmp",
full_name = "Math/adds",
jest_parts = { "Math", "adds" },
}
local cmd_spec = runner.build_command(spec)
assert.are.same(
{
"npx",
"jest",
"--testLocationInResults",
"--reporters",
get_reporter_path(),
"--runTestsByPath",
"/tmp/math.test.js",
"--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/math.test.js",
cwd = "/tmp",
kind = "describe",
jest_parts = { "", "renders properly..." },
}
local cmd_spec = runner.build_command(spec)
local pattern = cmd_spec.cmd[#cmd_spec.cmd]
assert.are.same(
{
"npx",
"jest",
"--testLocationInResults",
"--reporters",
get_reporter_path(),
"--runTestsByPath",
"/tmp/math.test.js",
"--testNamePattern",
pattern,
},
cmd_spec.cmd
)
assert.is_true(pattern:find("Header", 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(JEST_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",
"jest",
"--testLocationInResults",
"--reporters",
get_reporter_path(),
"--runTestsByPath",
root .. "/foo.test.ts",
},
cmd_spec.cmd
)
assert.equals(root, cmd_spec.cwd)
end)
end)
it("build_all_command runs project tests", function()
with_project(JEST_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",
"jest",
"--testLocationInResults",
"--reporters",
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",
"jest",
"--testLocationInResults",
"--reporters",
get_reporter_path(),
"--runTestsByPath",
"/tmp/math.test.js",
"--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",
"jest",
"--testLocationInResults",
"--reporters",
get_reporter_path(),
"--runTestsByPath",
"/tmp/math.test.js",
"--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 locations", function()
local output = table.concat({
"TSAMURAI_RESULT " .. vim.json.encode({
name = "Math/adds",
status = "passed",
file = "/tmp/math.test.js",
location = { line = 3, column = 4 },
}),
"TSAMURAI_RESULT " .. vim.json.encode({
name = "Math/subtracts",
status = "failed",
file = "/tmp/math.test.js",
location = { line = 10, column = 2 },
}),
"TSAMURAI_RESULT " .. vim.json.encode({
name = "Math/skipped",
status = "skipped",
file = "/tmp/math.test.js",
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("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.js",
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 per test", function()
local output = table.concat({
"TSAMURAI_RESULT " .. vim.json.encode({
name = "Math/adds",
status = "failed",
output = { "line1", "line2" },
}),
"TSAMURAI_RESULT " .. vim.json.encode({
name = "Math/adds",
status = "failed",
output = { "line3" },
}),
}, "\n")
local results = runner.parse_test_output(output)
assert.are.same({ "line1", "line2", "line3" }, results["Math/adds"])
end)
it("collect_failed_locations uses cached locations", function()
local output = "TSAMURAI_RESULT " .. vim.json.encode({
name = "Math/adds",
status = "failed",
file = "/tmp/math.test.js",
location = { line = 8, column = 2 },
})
runner.parse_results(output)
local items = runner.collect_failed_locations({ "Math/adds" }, nil, "file")
assert.equals(1, #items)
assert.equals("/tmp/math.test.js", items[1].filename)
assert.equals(8, items[1].lnum)
assert.equals(2, items[1].col)
end)
end)