Files
test-samurai.nvim/lua/test-samurai/runners/js.lua

1242 lines
35 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 append_args(cmd, args)
if not args or #args == 0 then
return
end
for _, arg in ipairs(args) do
table.insert(cmd, arg)
end
end
local function escape_regex(s)
s = s or ""
return (s:gsub("([\\.^$|()%%[%]{}*+?%-])", "\\\\%1"))
end
local function build_pattern(names)
local parts = {}
for _, name in ipairs(names or {}) do
if name and name ~= "" then
table.insert(parts, escape_regex(name))
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "|")
end
local function find_test_file_arg(cmd)
if not cmd then
return nil
end
for i = #cmd, 1, -1 do
local arg = cmd[i]
if type(arg) == "string" and arg:sub(1, 1) ~= "-" then
if arg:match("%.test%.[jt]sx?$") or arg:match("%.spec%.[jt]sx?$") then
return arg
end
end
end
return nil
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 build_full_name
local shorten_js_name
local normalize_js_name
local function add_location(target, name, file, line)
if not name or name == "" or not file or file == "" or not line then
return
end
if not target[name] then
target[name] = {}
end
table.insert(target[name], {
filename = file,
lnum = line,
col = 1,
text = name,
})
end
local function collect_js_locations(lines, file, target)
local describes, tests = collect_js_structs(lines)
for _, t in ipairs(tests) do
local full = build_full_name(lines, t.start + 1, t.name)
add_location(target, full, file, t.start + 1)
add_location(target, t.name, file, t.start + 1)
local short = shorten_js_name and shorten_js_name(full) or nil
if short and short ~= full then
add_location(target, short, file, t.start + 1)
end
end
for _, d in ipairs(describes) do
local full = build_full_name(lines, d.start + 1, d.name)
add_location(target, full, file, d.start + 1)
add_location(target, d.name, file, d.start + 1)
local short = shorten_js_name and shorten_js_name(full) or nil
if short and short ~= full then
add_location(target, short, file, d.start + 1)
end
end
end
local function collect_js_test_files(root, patterns)
if not root or root == "" then
root = vim.loop.cwd()
end
local files = {}
local seen = {}
for _, pat in ipairs(patterns or {}) do
local glob = "**/*" .. pat .. "*"
local hits = vim.fn.globpath(root, glob, false, true)
if type(hits) == "table" then
for _, file in ipairs(hits) do
if not seen[file] then
seen[file] = true
table.insert(files, file)
end
end
end
end
return files
end
build_full_name = function(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.all_glob = cfg.all_glob
runner.json_args = cfg.json_args or {}
runner.failed_only_flag = cfg.failed_only_flag
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)
append_args(cmd, runner.json_args)
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)
append_args(cmd, runner.json_args)
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)
append_args(cmd, runner.json_args)
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
function runner.build_file_command(bufnr)
local path = util.get_buf_path(bufnr)
if not path or path == "" then
return nil
end
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
local cmd = vim.deepcopy(runner.command)
append_args(cmd, runner.json_args)
table.insert(cmd, path)
return {
cmd = cmd,
cwd = root,
}
end
function runner.build_all_command(bufnr)
local path = util.get_buf_path(bufnr)
local root
if path and path ~= "" then
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
end
if not root or root == "" then
root = vim.loop.cwd()
end
local cmd = vim.deepcopy(runner.command)
append_args(cmd, runner.json_args)
if runner.framework == "mocha" and runner.all_glob then
table.insert(cmd, runner.all_glob)
end
return {
cmd = cmd,
cwd = root,
}
end
local function parse_jest_results(data)
if type(data) ~= "table" then
return nil
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
for _, result in ipairs(data.testResults or {}) do
for _, assertion in ipairs(result.assertionResults or {}) do
local full = assertion.fullName or assertion.title
local short = assertion.title or assertion.fullName
if assertion.status == "passed" and full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
elseif assertion.status == "failed" and full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
elseif (assertion.status == "pending" or assertion.status == "skipped" or assertion.status == "todo")
and full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
end
return { passes = passes, failures = failures, skips = skips, display = display }
end
local function split_output_lines(text)
if not text or text == "" then
return {}
end
local lines = vim.split(text, "\n", { plain = true })
if #lines > 0 and lines[#lines] == "" then
table.remove(lines, #lines)
end
return lines
end
local function append_output(out, name, text)
if not name or name == "" or not text or text == "" then
return
end
if not out[name] then
out[name] = {}
end
for _, line in ipairs(split_output_lines(text)) do
table.insert(out[name], line)
end
end
local function decode_json_from_output(output)
local ok, data = pcall(vim.json.decode, output)
if ok and type(data) == "table" then
return data
end
if not output or output == "" then
return nil
end
local start_idx = output:find("{", 1, true)
if not start_idx then
return nil
end
local end_idx = nil
for i = #output, start_idx, -1 do
if output:sub(i, i) == "}" then
end_idx = i
break
end
end
if not end_idx then
return nil
end
local candidate = output:sub(start_idx, end_idx)
local ok2, data2 = pcall(vim.json.decode, candidate)
if ok2 and type(data2) == "table" then
return data2
end
return nil
end
local function collect_jest_failure_output(data)
local out = {}
for _, result in ipairs(data.testResults or {}) do
for _, assertion in ipairs(result.assertionResults or {}) do
if assertion.status == "failed" then
local name = assertion.fullName or assertion.title
for _, msg in ipairs(assertion.failureMessages or {}) do
append_output(out, name, msg)
end
end
end
end
return out
end
local function collect_mocha_failure_output(output)
local out = {}
local ok, data = pcall(vim.json.decode, output)
if ok and type(data) == "table" then
for _, failure in ipairs(data.failures or {}) do
local name = failure.fullTitle or failure.title
local err = failure.err or failure
local message = nil
if type(err) == "table" then
message = err.stack or err.message
elseif type(err) == "string" then
message = err
end
append_output(out, name, message)
end
return out
end
for line in (output or ""):gmatch("[^\n]+") do
local ok_line, entry = pcall(vim.json.decode, line)
if ok_line and type(entry) == "table" then
local event = entry.event or entry[1] or entry["1"]
local payload = entry
if not entry.event then
payload = entry[2] or entry["2"] or {}
end
if event == "fail" then
local name = payload.fullTitle or payload.title
local err = payload.err or payload
local message = nil
if type(err) == "table" then
message = err.stack or err.message
elseif type(err) == "string" then
message = err
end
append_output(out, name, message)
end
end
end
return out
end
local function parse_jest_like(output)
local ok, data = pcall(vim.json.decode, output)
if ok and type(data) == "table" then
return parse_jest_results(data)
end
if not output or output == "" then
return nil
end
local start_idx = output:find("{", 1, true)
if not start_idx then
return nil
end
local end_idx = nil
for i = #output, start_idx, -1 do
if output:sub(i, i) == "}" then
end_idx = i
break
end
end
if not end_idx then
return nil
end
local candidate = output:sub(start_idx, end_idx)
local ok2, data2 = pcall(vim.json.decode, candidate)
if not ok2 or type(data2) ~= "table" then
return nil
end
return parse_jest_results(data2)
end
local jest_symbols = {
pass = string.char(0xE2, 0x9C, 0x93),
fail = string.char(0xE2, 0x9C, 0x95),
skip = string.char(0xE2, 0x97, 0x8B),
}
normalize_js_name = function(name)
if not name or name == "" then
return nil
end
local out = name
out = out:gsub("%s*[%>›»]%s*", " ")
out = out:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
return out
end
shorten_js_name = function(name)
if not name or name == "" then
return nil
end
local last = name:match(".*>%s*([^>]+)$")
if last then
return last:gsub("^%s+", ""):gsub("%s+$", "")
end
last = name:match(".*%s*([^]+)$")
if last then
return last:gsub("^%s+", ""):gsub("%s+$", "")
end
last = name:match(".*»%s*([^»]+)$")
if last then
return last:gsub("^%s+", ""):gsub("%s+$", "")
end
return name
end
local function trim_jest_verbose_name(name)
if not name then
return nil
end
name = name:gsub("%s+%(%d+%.?%d*%s*ms%)%s*$", "")
name = name:gsub("%s+%(%d+%.?%d*%s*sec%)%s*$", "")
name = name:gsub("%s+%b()%s*$", "")
name = name:gsub("%s+$", "")
return name
end
local function parse_jest_verbose_line(line)
if not line or line == "" then
return nil, nil
end
local name = line:match("^%s*" .. jest_symbols.pass .. "%s+(.+)$")
if name then
return "pass", trim_jest_verbose_name(name)
end
name = line:match("^%s*" .. jest_symbols.fail .. "%s+(.+)$")
if name then
return "fail", trim_jest_verbose_name(name)
end
name = line:match("^%s*" .. jest_symbols.skip .. "%s+(.+)$")
if name then
return "skip", trim_jest_verbose_name(name)
end
return nil, nil
end
local function parse_mocha(output)
local ok, data = pcall(vim.json.decode, output)
if not ok or type(data) ~= "table" then
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
for line in (output or ""):gmatch("[^\n]+") do
local ok_line, entry = pcall(vim.json.decode, line)
if ok_line and type(entry) == "table" then
local event = entry.event or entry[1] or entry["1"]
local payload = entry
if not entry.event then
payload = entry[2] or entry["2"] or {}
end
local full = payload.fullTitle or payload.title
local short = payload.title or payload.fullTitle
if event == "pass" and full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
elseif event == "fail" and full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
elseif event == "pending" and full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
end
if #passes == 0 and #failures == 0 and #skips == 0 then
return nil
end
return { passes = passes, failures = failures, skips = skips, display = display }
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
if type(data.tests) == "table" then
for _, test in ipairs(data.tests) do
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if test.state == "passed" and full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
elseif test.state == "failed" and full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
elseif test.state == "pending" and full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
elseif type(data.passes) == "table" or type(data.failures) == "table" then
for _, test in ipairs(data.passes or {}) do
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
end
end
for _, test in ipairs(data.failures or {}) do
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
end
end
for _, test in ipairs(data.pending or {}) do
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
end
return { passes = passes, failures = failures, skips = skips, display = display }
end
local function parse_output(output)
if runner.framework == "mocha" then
return parse_mocha(output)
end
return parse_jest_like(output)
end
function runner.parse_results(output)
return parse_output(output)
end
function runner.parse_test_output(output)
if runner.framework == "mocha" then
return collect_mocha_failure_output(output)
end
local data = decode_json_from_output(output)
if not data then
return {}
end
return collect_jest_failure_output(data)
end
function runner.output_parser()
local state = { raw = {}, done = false, saw_stream = false }
local failures = {}
local skips = {}
local jest_seen_pass = {}
local jest_seen_fail = {}
local jest_seen_skip = {}
local jest_json_collecting = false
local jest_json_buffer = {}
local jest_streamed = false
local function reset_list(dst, src)
for i = #dst, 1, -1 do
dst[i] = nil
end
for _, item in ipairs(src or {}) do
if item and item ~= "" then
table.insert(dst, item)
end
end
end
local function emit_jest_results(results, emit_output)
if not results then
return nil
end
local display = results.display or {}
if emit_output == false then
reset_list(failures, results.failures or {})
return {
passes = {},
failures = {},
skips = {},
failures_all = vim.deepcopy(failures),
}
end
local out = {
passes = {},
failures = {},
skips = {},
failures_all = {},
display = { passes = {}, failures = {}, skips = {} },
}
for i, title in ipairs(results.passes or {}) do
if title and not jest_seen_pass[title] then
jest_seen_pass[title] = true
table.insert(out.passes, title)
local show = display.passes and display.passes[i] or title
table.insert(out.display.passes, show)
end
end
for i, title in ipairs(results.failures or {}) do
if title and not jest_seen_fail[title] then
jest_seen_fail[title] = true
table.insert(failures, title)
table.insert(out.failures, title)
local show = display.failures and display.failures[i] or title
table.insert(out.display.failures, show)
end
end
for i, title in ipairs(results.skips or {}) do
if title and not jest_seen_skip[title] then
jest_seen_skip[title] = true
table.insert(out.skips, title)
local show = display.skips and display.skips[i] or title
table.insert(out.display.skips, show)
end
end
out.failures_all = vim.deepcopy(failures)
if #out.passes == 0 and #out.failures == 0 and #out.skips == 0 then
return nil
end
return out
end
local function feed_jest_json(line)
if not jest_json_collecting then
if not line:match("^%s*{") then
return nil
end
jest_json_collecting = true
jest_json_buffer = { line }
else
table.insert(jest_json_buffer, line)
end
local candidate = table.concat(jest_json_buffer, "\n")
local ok_buf, data_buf = pcall(vim.json.decode, candidate)
if ok_buf and type(data_buf) == "table" then
jest_json_collecting = false
jest_json_buffer = {}
return data_buf
end
return nil
end
local function handle_jest_json(data)
local results = parse_jest_results(data)
if not results then
return nil
end
state.done = true
state.saw_stream = true
return emit_jest_results(results, not jest_streamed)
end
return {
on_line = function(line, _state)
if state.done then
return nil
end
if runner.framework == "jest" then
local kind, name = parse_jest_verbose_line(line)
if kind and name then
jest_streamed = true
local results = { passes = {}, failures = {}, skips = {} }
if kind == "pass" then
results.passes = { name }
elseif kind == "fail" then
results.failures = { name }
elseif kind == "skip" then
results.skips = { name }
end
results.display = {
passes = kind == "pass" and { name } or {},
failures = kind == "fail" and { name } or {},
skips = kind == "skip" and { name } or {},
}
return emit_jest_results(results, true)
end
local data = feed_jest_json(line)
if data then
return handle_jest_json(data)
end
return nil
end
if runner.framework == "mocha" and runner.json_args then
local uses_stream = false
for i = 1, #runner.json_args - 1 do
if runner.json_args[i] == "--reporter" and runner.json_args[i + 1] == "json-stream" then
uses_stream = true
break
end
end
if uses_stream then
local ok, data = pcall(vim.json.decode, line)
if ok and type(data) == "table" then
local event = data.event
local payload = data
if not event then
event = data[1] or data["1"]
payload = data[2] or data["2"] or {}
end
if event == "pass" and payload.fullTitle then
state.saw_stream = true
return {
passes = { payload.fullTitle },
failures = {},
skips = {},
display = {
passes = { payload.title or payload.fullTitle },
failures = {},
skips = {},
},
failures_all = vim.deepcopy(failures),
}
elseif event == "fail" and payload.fullTitle then
state.saw_stream = true
table.insert(failures, payload.fullTitle)
return {
passes = {},
failures = { payload.fullTitle },
skips = {},
display = {
passes = {},
failures = { payload.title or payload.fullTitle },
skips = {},
},
failures_all = vim.deepcopy(failures),
}
elseif event == "pending" and payload.fullTitle then
state.saw_stream = true
table.insert(skips, payload.fullTitle)
return {
passes = {},
failures = {},
skips = { payload.fullTitle },
display = {
passes = {},
failures = {},
skips = { payload.title or payload.fullTitle },
},
failures_all = vim.deepcopy(failures),
}
elseif event == "start" or event == "end" then
state.saw_stream = true
return {
passes = {},
failures = {},
skips = {},
failures_all = vim.deepcopy(failures),
}
end
end
return nil
end
end
if runner.framework == "vitest" and runner.json_args then
local uses_tap = false
for i = 1, #runner.json_args - 1 do
if runner.json_args[i] == "--reporter" and (runner.json_args[i + 1] == "tap" or runner.json_args[i + 1] == "tap-flat") then
uses_tap = true
break
end
end
if uses_tap then
local ok_title = line:match("^ok%s+%d+%s+%-%s+(.+)")
if ok_title then
local skip_title = ok_title:match("^(.-)%s+#%s+SKIP.*$")
if skip_title then
skip_title = skip_title:gsub("%s+$", "")
table.insert(skips, skip_title)
state.saw_stream = true
return {
passes = {},
failures = {},
skips = { skip_title },
display = {
passes = {},
failures = {},
skips = { shorten_js_name(skip_title) or skip_title },
},
failures_all = vim.deepcopy(failures),
}
end
ok_title = ok_title:gsub("%s+#%s+time=.*$", "")
state.saw_stream = true
return {
passes = { ok_title },
failures = {},
skips = {},
display = {
passes = { shorten_js_name(ok_title) or ok_title },
failures = {},
skips = {},
},
failures_all = vim.deepcopy(failures),
}
end
local fail_title = line:match("^not ok%s+%d+%s+%-%s+(.+)")
if fail_title then
fail_title = fail_title:gsub("%s+#%s+time=.*$", "")
local display_name = shorten_js_name(fail_title)
table.insert(failures, fail_title)
state.saw_stream = true
return {
passes = {},
failures = { fail_title },
skips = {},
display = {
passes = {},
failures = { display_name or fail_title },
skips = {},
},
failures_all = vim.deepcopy(failures),
}
end
return nil
end
end
table.insert(state.raw, line)
local output = table.concat(state.raw, "\n")
local results = parse_output(output)
if results then
state.done = true
end
return results
end,
on_complete = function(output, _state)
if runner.framework == "jest" then
if state.done then
return nil
end
local results = parse_jest_like(output)
if results then
state.done = true
state.saw_stream = true
if _state and _state.scope_kind == "all" and jest_streamed then
return emit_jest_results(results, false)
end
return emit_jest_results(results, not jest_streamed)
end
return nil
end
if state.done or state.saw_stream then
return nil
end
local results = parse_output(output)
if results then
state.done = true
end
return results
end,
}
end
function runner.build_failed_command(last_command, failures, scope_kind)
local pattern = build_pattern(failures)
if not pattern then
return nil
end
local cmd = vim.deepcopy(runner.command)
append_args(cmd, runner.json_args)
if runner.framework == "mocha" then
if #failures == 1 then
table.insert(cmd, "--fgrep")
table.insert(cmd, failures[1])
else
table.insert(cmd, "--grep")
table.insert(cmd, pattern)
end
else
table.insert(cmd, "-t")
table.insert(cmd, pattern)
end
if scope_kind ~= "all" and last_command then
local file = find_test_file_arg(last_command.cmd)
if file then
table.insert(cmd, file)
end
end
return {
cmd = cmd,
cwd = last_command and last_command.cwd or nil,
}
end
function runner.collect_failed_locations(failures, command, scope_kind)
if type(failures) ~= "table" or #failures == 0 then
return {}
end
local files = {}
if scope_kind == "all" then
files = collect_js_test_files(command and command.cwd or nil, runner.patterns)
else
local file = command and command.file or nil
if not file and command and type(command.cmd) == "table" then
file = find_test_file_arg(command.cmd)
end
if file then
files = { file }
end
end
if #files == 0 then
return {}
end
local locations = {}
for _, file in ipairs(files) do
local ok, lines = pcall(vim.fn.readfile, file)
if ok and type(lines) == "table" then
collect_js_locations(lines, file, locations)
end
end
local items = {}
local seen = {}
for _, name in ipairs(failures) do
local keys = { name }
if normalize_js_name then
local normalized = normalize_js_name(name)
if normalized and normalized ~= name then
table.insert(keys, normalized)
end
end
if shorten_js_name then
local short = shorten_js_name(name)
if short and short ~= name then
table.insert(keys, short)
end
end
for _, key_name in ipairs(keys) do
for _, loc in ipairs(locations[key_name] or {}) do
local key = string.format("%s:%d", loc.filename or "", loc.lnum or 0)
if not seen[key] then
seen[key] = true
table.insert(items, loc)
end
end
end
end
return items
end
return runner
end
return M