319 lines
7.4 KiB
Lua
319 lines
7.4 KiB
Lua
local util = require("test-samurai.util")
|
|
|
|
local M = {}
|
|
|
|
local default_filetypes = {
|
|
javascript = true,
|
|
javascriptreact = true,
|
|
typescript = true,
|
|
typescriptreact = true,
|
|
}
|
|
|
|
local default_patterns = { ".test.", ".spec." }
|
|
|
|
local function is_js_test_file(bufnr, filetypes, patterns)
|
|
local ft = vim.bo[bufnr].filetype
|
|
if not filetypes[ft] then
|
|
return false
|
|
end
|
|
local path = util.get_buf_path(bufnr)
|
|
if not path or path == "" then
|
|
return false
|
|
end
|
|
for _, pat in ipairs(patterns) do
|
|
if path:find(pat, 1, true) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function find_block_end(lines, start_idx)
|
|
local depth = 0
|
|
local started = false
|
|
for i = start_idx, #lines do
|
|
local line = lines[i]
|
|
for j = 1, #line do
|
|
local ch = line:sub(j, j)
|
|
if ch == "{" then
|
|
depth = depth + 1
|
|
started = true
|
|
elseif ch == "}" then
|
|
if started then
|
|
depth = depth - 1
|
|
if depth == 0 then
|
|
return i - 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return #lines - 1
|
|
end
|
|
|
|
local jest_config_files = { "jest.config.js", "jest.config.ts" }
|
|
|
|
local function find_jest_root(file)
|
|
if not file or file == "" then
|
|
return util.find_root(file, {
|
|
"jest.config.js",
|
|
"jest.config.ts",
|
|
"vitest.config.ts",
|
|
"vitest.config.js",
|
|
"package.json",
|
|
"node_modules",
|
|
})
|
|
end
|
|
|
|
local dir = vim.fs.dirname(file)
|
|
local found = vim.fs.find(jest_config_files, { path = dir, upward = true })
|
|
if found and #found > 0 then
|
|
local cfg = found[1]
|
|
local sep = package.config:sub(1, 1)
|
|
local marker = sep .. "test" .. sep .. ".bin" .. sep
|
|
local idx = cfg:find(marker, 1, true)
|
|
if idx then
|
|
local root = cfg:sub(1, idx - 1)
|
|
if root == "" then
|
|
root = sep
|
|
end
|
|
return root
|
|
end
|
|
return vim.fs.dirname(cfg)
|
|
end
|
|
|
|
return util.find_root(file, {
|
|
"jest.config.js",
|
|
"jest.config.ts",
|
|
"vitest.config.ts",
|
|
"vitest.config.js",
|
|
"package.json",
|
|
"node_modules",
|
|
})
|
|
end
|
|
|
|
local function match_test_call(line)
|
|
local call, name = line:match("^%s*(it)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
if call then
|
|
return call, name
|
|
end
|
|
call, name = line:match("^%s*(test)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
if call then
|
|
return call, name
|
|
end
|
|
call, name = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
if call and name then
|
|
return call, name
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function collect_js_structs(lines)
|
|
local describes = {}
|
|
local tests = {}
|
|
|
|
for i, line in ipairs(lines) do
|
|
local dcall, dname = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
if dcall and dname then
|
|
local start0 = i - 1
|
|
local end0 = find_block_end(lines, i)
|
|
table.insert(describes, {
|
|
kind = dcall,
|
|
name = dname,
|
|
start = start0,
|
|
["end"] = end0,
|
|
})
|
|
else
|
|
local tcall, tname = line:match("^%s*(it)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
if not tcall then
|
|
tcall, tname = line:match("^%s*(test)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
end
|
|
if tcall and tname then
|
|
local start0 = i - 1
|
|
local end0 = find_block_end(lines, i)
|
|
table.insert(tests, {
|
|
kind = tcall,
|
|
name = tname,
|
|
start = start0,
|
|
["end"] = end0,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
return describes, tests
|
|
end
|
|
|
|
local function build_full_name(lines, idx, leaf_name)
|
|
local parts = { leaf_name }
|
|
for i = idx - 1, 1, -1 do
|
|
local line = lines[i]
|
|
local call, name = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
|
if call and name then
|
|
table.insert(parts, 1, name)
|
|
end
|
|
end
|
|
return table.concat(parts, " ")
|
|
end
|
|
|
|
local function find_nearest_test(bufnr, row)
|
|
local lines = util.get_buf_lines(bufnr)
|
|
local describes, tests = collect_js_structs(lines)
|
|
|
|
for _, t in ipairs(tests) do
|
|
if row >= t.start and row <= t["end"] then
|
|
local full = build_full_name(lines, t.start + 1, t.name)
|
|
return {
|
|
kind = t.kind,
|
|
name = t.name,
|
|
full_name = full,
|
|
line = t.start,
|
|
}
|
|
end
|
|
end
|
|
|
|
local best_describe = nil
|
|
for _, d in ipairs(describes) do
|
|
if row >= d.start and row <= d["end"] then
|
|
if not best_describe or d.start >= best_describe.start then
|
|
best_describe = d
|
|
end
|
|
end
|
|
end
|
|
if best_describe then
|
|
local full = build_full_name(lines, best_describe.start + 1, best_describe.name)
|
|
return {
|
|
kind = "describe",
|
|
name = best_describe.name,
|
|
full_name = full,
|
|
line = best_describe.start,
|
|
}
|
|
end
|
|
|
|
local start = row + 1
|
|
if start > #lines then
|
|
start = #lines
|
|
elseif start < 1 then
|
|
start = 1
|
|
end
|
|
for i = start, 1, -1 do
|
|
local line = lines[i]
|
|
local call, name = match_test_call(line)
|
|
if call and name then
|
|
local full = build_full_name(lines, i, name)
|
|
return {
|
|
kind = call,
|
|
name = name,
|
|
full_name = full,
|
|
line = i - 1,
|
|
}
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function M.new(opts)
|
|
local cfg = opts or {}
|
|
local runner = {}
|
|
|
|
runner.name = cfg.name or "js"
|
|
runner.framework = cfg.framework or "jest"
|
|
runner.command = cfg.command or { "npx", runner.framework }
|
|
|
|
runner.filetypes = {}
|
|
if cfg.filetypes then
|
|
for _, ft in ipairs(cfg.filetypes) do
|
|
runner.filetypes[ft] = true
|
|
end
|
|
else
|
|
runner.filetypes = vim.deepcopy(default_filetypes)
|
|
end
|
|
|
|
runner.patterns = cfg.patterns or default_patterns
|
|
|
|
function runner.is_test_file(bufnr)
|
|
return is_js_test_file(bufnr, runner.filetypes, runner.patterns)
|
|
end
|
|
|
|
function runner.find_nearest(bufnr, row, _col)
|
|
if not runner.is_test_file(bufnr) then
|
|
return nil, "not a JS/TS test file"
|
|
end
|
|
local hit = find_nearest_test(bufnr, row)
|
|
if not hit then
|
|
return nil, "no test call found"
|
|
end
|
|
local path = util.get_buf_path(bufnr)
|
|
local root
|
|
if runner.framework == "jest" then
|
|
root = find_jest_root(path)
|
|
else
|
|
root = util.find_root(path, {
|
|
"jest.config.js",
|
|
"jest.config.ts",
|
|
"vitest.config.ts",
|
|
"vitest.config.js",
|
|
"package.json",
|
|
"node_modules",
|
|
})
|
|
end
|
|
return {
|
|
file = path,
|
|
cwd = root,
|
|
framework = runner.framework,
|
|
test_name = hit.name,
|
|
full_name = hit.full_name,
|
|
kind = hit.kind,
|
|
}
|
|
end
|
|
|
|
local function build_jest(spec)
|
|
local cmd = vim.deepcopy(runner.command)
|
|
table.insert(cmd, spec.file)
|
|
table.insert(cmd, "-t")
|
|
table.insert(cmd, spec.test_name)
|
|
return cmd
|
|
end
|
|
|
|
local function build_mocha(spec)
|
|
local cmd = vim.deepcopy(runner.command)
|
|
local target = spec.full_name or spec.test_name
|
|
table.insert(cmd, "--fgrep")
|
|
table.insert(cmd, target)
|
|
table.insert(cmd, spec.file)
|
|
return cmd
|
|
end
|
|
|
|
local function build_vitest(spec)
|
|
local cmd = vim.deepcopy(runner.command)
|
|
table.insert(cmd, spec.file)
|
|
table.insert(cmd, "-t")
|
|
table.insert(cmd, spec.test_name)
|
|
return cmd
|
|
end
|
|
|
|
function runner.build_command(spec)
|
|
local fw = runner.framework
|
|
local cmd
|
|
if fw == "jest" then
|
|
cmd = build_jest(spec)
|
|
elseif fw == "mocha" then
|
|
cmd = build_mocha(spec)
|
|
elseif fw == "vitest" then
|
|
cmd = build_vitest(spec)
|
|
else
|
|
cmd = vim.deepcopy(runner.command)
|
|
table.insert(cmd, spec.file)
|
|
end
|
|
return {
|
|
cmd = cmd,
|
|
cwd = spec.cwd,
|
|
}
|
|
end
|
|
|
|
return runner
|
|
end
|
|
|
|
return M
|