extend simple test listing to a list-detail-view window
This commit is contained in:
@@ -15,11 +15,22 @@ local state = {
|
|||||||
last_scope_exit_code = nil,
|
last_scope_exit_code = nil,
|
||||||
last_scope_failures = nil,
|
last_scope_failures = nil,
|
||||||
last_border_kind = "default",
|
last_border_kind = "default",
|
||||||
|
last_test_outputs = {},
|
||||||
|
last_result_line_map = {},
|
||||||
|
last_raw_output = nil,
|
||||||
|
last_float = nil,
|
||||||
|
detail_buf = nil,
|
||||||
|
detail_win = nil,
|
||||||
|
detail_opening = false,
|
||||||
autocmds_set = false,
|
autocmds_set = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary")
|
local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary")
|
||||||
local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult")
|
local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult")
|
||||||
|
local detail_ns = vim.api.nvim_create_namespace("TestSamuraiDetailAnsi")
|
||||||
|
local apply_border_kind
|
||||||
|
local close_container
|
||||||
|
local restore_listing_full
|
||||||
|
|
||||||
local function get_hl_fg(name)
|
local function get_hl_fg(name)
|
||||||
local ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = name, link = true })
|
local ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = name, link = true })
|
||||||
@@ -79,12 +90,33 @@ local function ensure_output_autocmds()
|
|||||||
vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter" }, {
|
vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter" }, {
|
||||||
group = group,
|
group = group,
|
||||||
callback = function()
|
callback = function()
|
||||||
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
if state.detail_opening then
|
||||||
local cur = vim.api.nvim_get_current_win()
|
return
|
||||||
if cur ~= state.last_win then
|
end
|
||||||
pcall(vim.api.nvim_win_close, state.last_win, true)
|
local cur = vim.api.nvim_get_current_win()
|
||||||
state.last_win = nil
|
local keep = (state.last_win and vim.api.nvim_win_is_valid(state.last_win) and cur == state.last_win)
|
||||||
end
|
or (state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) and cur == state.detail_win)
|
||||||
|
if not keep then
|
||||||
|
close_container()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("WinClosed", {
|
||||||
|
group = group,
|
||||||
|
callback = function(args)
|
||||||
|
local closed = tonumber(args.match)
|
||||||
|
if not closed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if state.last_win and closed == state.last_win then
|
||||||
|
state.last_win = nil
|
||||||
|
close_container()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if state.detail_win and closed == state.detail_win then
|
||||||
|
state.detail_win = nil
|
||||||
|
restore_listing_full()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
@@ -111,6 +143,12 @@ function M.setup()
|
|||||||
state.last_scope_exit_code = nil
|
state.last_scope_exit_code = nil
|
||||||
state.last_scope_failures = nil
|
state.last_scope_failures = nil
|
||||||
state.last_border_kind = "default"
|
state.last_border_kind = "default"
|
||||||
|
state.last_test_outputs = {}
|
||||||
|
state.last_result_line_map = {}
|
||||||
|
state.last_raw_output = nil
|
||||||
|
state.last_float = nil
|
||||||
|
state.detail_opening = false
|
||||||
|
state.detail_opening = false
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.reload_runners()
|
function M.reload_runners()
|
||||||
@@ -223,7 +261,7 @@ local function float_geometry()
|
|||||||
return width, height, row, col
|
return width, height, row, col
|
||||||
end
|
end
|
||||||
|
|
||||||
local function apply_border_kind(win, kind)
|
apply_border_kind = function(win, kind)
|
||||||
if not (win and vim.api.nvim_win_is_valid(win)) then
|
if not (win and vim.api.nvim_win_is_valid(win)) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -236,11 +274,46 @@ local function apply_border_kind(win, kind)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
close_container = function()
|
||||||
|
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
||||||
|
pcall(vim.api.nvim_win_close, state.last_win, true)
|
||||||
|
state.last_win = nil
|
||||||
|
end
|
||||||
|
if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then
|
||||||
|
pcall(vim.api.nvim_win_close, state.detail_win, true)
|
||||||
|
state.detail_win = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
restore_listing_full = function()
|
||||||
|
if not (state.last_win and vim.api.nvim_win_is_valid(state.last_win)) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local width = state.last_float and state.last_float.width or math.floor(vim.o.columns * 0.8)
|
||||||
|
local height = state.last_float and state.last_float.height or math.floor(vim.o.lines * 0.8)
|
||||||
|
local row = state.last_float and state.last_float.row or math.floor((vim.o.lines - height) / 2)
|
||||||
|
local col = state.last_float and state.last_float.col or math.floor((vim.o.columns - width) / 2)
|
||||||
|
vim.api.nvim_win_set_config(state.last_win, {
|
||||||
|
relative = "editor",
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
row = row,
|
||||||
|
col = col,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded",
|
||||||
|
})
|
||||||
|
apply_border_kind(state.last_win, state.last_border_kind)
|
||||||
|
end
|
||||||
|
|
||||||
local function create_output_win(initial_lines)
|
local function create_output_win(initial_lines)
|
||||||
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
||||||
pcall(vim.api.nvim_win_close, state.last_win, true)
|
pcall(vim.api.nvim_win_close, state.last_win, true)
|
||||||
state.last_win = nil
|
state.last_win = nil
|
||||||
end
|
end
|
||||||
|
if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then
|
||||||
|
pcall(vim.api.nvim_win_close, state.detail_win, true)
|
||||||
|
state.detail_win = nil
|
||||||
|
end
|
||||||
|
|
||||||
local buf = state.last_buf
|
local buf = state.last_buf
|
||||||
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
||||||
@@ -253,6 +326,7 @@ local function create_output_win(initial_lines)
|
|||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_lines or {})
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_lines or {})
|
||||||
|
|
||||||
local width, height, row, col = float_geometry()
|
local width, height, row, col = float_geometry()
|
||||||
|
state.last_float = { width = width, height = height, row = row, col = col }
|
||||||
|
|
||||||
local win = vim.api.nvim_open_win(buf, true, {
|
local win = vim.api.nvim_open_win(buf, true, {
|
||||||
relative = "editor",
|
relative = "editor",
|
||||||
@@ -265,12 +339,10 @@ local function create_output_win(initial_lines)
|
|||||||
})
|
})
|
||||||
|
|
||||||
vim.keymap.set("n", "<esc><esc>", function()
|
vim.keymap.set("n", "<esc><esc>", function()
|
||||||
if vim.api.nvim_win_is_valid(win) then
|
close_container()
|
||||||
vim.api.nvim_win_close(win, true)
|
end, { buffer = buf, nowait = true, silent = true })
|
||||||
if state.last_win == win then
|
vim.keymap.set("n", "<cr>", function()
|
||||||
state.last_win = nil
|
M.open_test_output_at_cursor()
|
||||||
end
|
|
||||||
end
|
|
||||||
end, { buffer = buf, nowait = true, silent = true })
|
end, { buffer = buf, nowait = true, silent = true })
|
||||||
|
|
||||||
state.last_win = win
|
state.last_win = win
|
||||||
@@ -289,8 +361,13 @@ local function reopen_output_win()
|
|||||||
vim.api.nvim_set_current_win(state.last_win)
|
vim.api.nvim_set_current_win(state.last_win)
|
||||||
return state.last_buf, state.last_win
|
return state.last_buf, state.last_win
|
||||||
end
|
end
|
||||||
|
if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then
|
||||||
|
pcall(vim.api.nvim_win_close, state.detail_win, true)
|
||||||
|
state.detail_win = nil
|
||||||
|
end
|
||||||
|
|
||||||
local width, height, row, col = float_geometry()
|
local width, height, row, col = float_geometry()
|
||||||
|
state.last_float = { width = width, height = height, row = row, col = col }
|
||||||
|
|
||||||
local win = vim.api.nvim_open_win(state.last_buf, true, {
|
local win = vim.api.nvim_open_win(state.last_buf, true, {
|
||||||
relative = "editor",
|
relative = "editor",
|
||||||
@@ -303,12 +380,10 @@ local function reopen_output_win()
|
|||||||
})
|
})
|
||||||
|
|
||||||
vim.keymap.set("n", "<esc><esc>", function()
|
vim.keymap.set("n", "<esc><esc>", function()
|
||||||
if vim.api.nvim_win_is_valid(win) then
|
close_container()
|
||||||
vim.api.nvim_win_close(win, true)
|
end, { buffer = state.last_buf, nowait = true, silent = true })
|
||||||
if state.last_win == win then
|
vim.keymap.set("n", "<cr>", function()
|
||||||
state.last_win = nil
|
M.open_test_output_at_cursor()
|
||||||
end
|
|
||||||
end
|
|
||||||
end, { buffer = state.last_buf, nowait = true, silent = true })
|
end, { buffer = state.last_buf, nowait = true, silent = true })
|
||||||
|
|
||||||
state.last_win = win
|
state.last_win = win
|
||||||
@@ -333,6 +408,398 @@ local function append_lines(buf, new_lines)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function normalize_output_lines(lines)
|
||||||
|
if type(lines) ~= "table" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
if line ~= nil then
|
||||||
|
table.insert(out, line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ansi_color_to_rgb(idx)
|
||||||
|
local basic = {
|
||||||
|
[0] = 0x000000,
|
||||||
|
[1] = 0x800000,
|
||||||
|
[2] = 0x008000,
|
||||||
|
[3] = 0x808000,
|
||||||
|
[4] = 0x000080,
|
||||||
|
[5] = 0x800080,
|
||||||
|
[6] = 0x008080,
|
||||||
|
[7] = 0xC0C0C0,
|
||||||
|
[8] = 0x808080,
|
||||||
|
[9] = 0xFF0000,
|
||||||
|
[10] = 0x00FF00,
|
||||||
|
[11] = 0xFFFF00,
|
||||||
|
[12] = 0x0000FF,
|
||||||
|
[13] = 0xFF00FF,
|
||||||
|
[14] = 0x00FFFF,
|
||||||
|
[15] = 0xFFFFFF,
|
||||||
|
}
|
||||||
|
if basic[idx] then
|
||||||
|
return basic[idx]
|
||||||
|
end
|
||||||
|
if idx >= 16 and idx <= 231 then
|
||||||
|
local v = idx - 16
|
||||||
|
local r = math.floor(v / 36)
|
||||||
|
local g = math.floor((v % 36) / 6)
|
||||||
|
local b = v % 6
|
||||||
|
local function comp(n)
|
||||||
|
if n == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return 55 + (n * 40)
|
||||||
|
end
|
||||||
|
return comp(r) * 0x10000 + comp(g) * 0x100 + comp(b)
|
||||||
|
end
|
||||||
|
if idx >= 232 and idx <= 255 then
|
||||||
|
local shade = 8 + (idx - 232) * 10
|
||||||
|
return shade * 0x10000 + shade * 0x100 + shade
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_ansi_hl(style, cache)
|
||||||
|
local fg = style.fg
|
||||||
|
local bg = style.bg
|
||||||
|
if style.reverse then
|
||||||
|
fg, bg = bg, fg
|
||||||
|
end
|
||||||
|
local key = table.concat({
|
||||||
|
tostring(fg or ""),
|
||||||
|
tostring(bg or ""),
|
||||||
|
style.bold and "b" or "",
|
||||||
|
style.italic and "i" or "",
|
||||||
|
style.underline and "u" or "",
|
||||||
|
}, "|")
|
||||||
|
if key == "||||" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if cache[key] then
|
||||||
|
return cache[key]
|
||||||
|
end
|
||||||
|
local name = "TestSamuraiAnsi_" .. tostring(#cache + 1)
|
||||||
|
cache[key] = name
|
||||||
|
local opts = {}
|
||||||
|
if fg then
|
||||||
|
opts.fg = fg
|
||||||
|
end
|
||||||
|
if bg then
|
||||||
|
opts.bg = bg
|
||||||
|
end
|
||||||
|
if style.bold then
|
||||||
|
opts.bold = true
|
||||||
|
end
|
||||||
|
if style.italic then
|
||||||
|
opts.italic = true
|
||||||
|
end
|
||||||
|
if style.underline then
|
||||||
|
opts.underline = true
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_set_hl, 0, name, opts)
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_ansi_lines(lines)
|
||||||
|
local clean_lines = {}
|
||||||
|
local highlights = {}
|
||||||
|
local style = {}
|
||||||
|
local cache = {}
|
||||||
|
|
||||||
|
local function apply_sgr(params)
|
||||||
|
if #params == 0 then
|
||||||
|
params = { 0 }
|
||||||
|
end
|
||||||
|
local i = 1
|
||||||
|
while i <= #params do
|
||||||
|
local p = params[i]
|
||||||
|
if p == 0 then
|
||||||
|
style = {}
|
||||||
|
elseif p == 1 then
|
||||||
|
style.bold = true
|
||||||
|
elseif p == 2 then
|
||||||
|
style.dim = true
|
||||||
|
elseif p == 3 then
|
||||||
|
style.italic = true
|
||||||
|
elseif p == 4 then
|
||||||
|
style.underline = true
|
||||||
|
elseif p == 7 then
|
||||||
|
style.reverse = true
|
||||||
|
elseif p == 22 then
|
||||||
|
style.bold = nil
|
||||||
|
style.dim = nil
|
||||||
|
elseif p == 23 then
|
||||||
|
style.italic = nil
|
||||||
|
elseif p == 24 then
|
||||||
|
style.underline = nil
|
||||||
|
elseif p == 27 then
|
||||||
|
style.reverse = nil
|
||||||
|
elseif p == 39 then
|
||||||
|
style.fg = nil
|
||||||
|
elseif p == 49 then
|
||||||
|
style.bg = nil
|
||||||
|
elseif p >= 30 and p <= 37 then
|
||||||
|
style.fg = ansi_color_to_rgb(p - 30)
|
||||||
|
elseif p >= 90 and p <= 97 then
|
||||||
|
style.fg = ansi_color_to_rgb(p - 90 + 8)
|
||||||
|
elseif p >= 40 and p <= 47 then
|
||||||
|
style.bg = ansi_color_to_rgb(p - 40)
|
||||||
|
elseif p >= 100 and p <= 107 then
|
||||||
|
style.bg = ansi_color_to_rgb(p - 100 + 8)
|
||||||
|
elseif p == 38 or p == 48 then
|
||||||
|
local is_fg = (p == 38)
|
||||||
|
local mode = params[i + 1]
|
||||||
|
if mode == 5 then
|
||||||
|
local idx = params[i + 2]
|
||||||
|
if idx then
|
||||||
|
local rgb = ansi_color_to_rgb(idx)
|
||||||
|
if is_fg then
|
||||||
|
style.fg = rgb
|
||||||
|
else
|
||||||
|
style.bg = rgb
|
||||||
|
end
|
||||||
|
end
|
||||||
|
i = i + 2
|
||||||
|
elseif mode == 2 then
|
||||||
|
local r = params[i + 2]
|
||||||
|
local g = params[i + 3]
|
||||||
|
local b = params[i + 4]
|
||||||
|
if r and g and b then
|
||||||
|
local rgb = r * 0x10000 + g * 0x100 + b
|
||||||
|
if is_fg then
|
||||||
|
style.fg = rgb
|
||||||
|
else
|
||||||
|
style.bg = rgb
|
||||||
|
end
|
||||||
|
end
|
||||||
|
i = i + 4
|
||||||
|
end
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for lnum, line in ipairs(lines or {}) do
|
||||||
|
local clean = {}
|
||||||
|
local hls = {}
|
||||||
|
local pos = 1
|
||||||
|
local col = 0
|
||||||
|
while true do
|
||||||
|
local s, e = line:find("\27%[[0-9;]*m", pos)
|
||||||
|
if not s then
|
||||||
|
local chunk = line:sub(pos)
|
||||||
|
if chunk ~= "" then
|
||||||
|
table.insert(clean, chunk)
|
||||||
|
local hl = build_ansi_hl(style, cache)
|
||||||
|
if hl then
|
||||||
|
table.insert(hls, { col, col + #chunk, hl })
|
||||||
|
end
|
||||||
|
col = col + #chunk
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
local chunk = line:sub(pos, s - 1)
|
||||||
|
if chunk ~= "" then
|
||||||
|
table.insert(clean, chunk)
|
||||||
|
local hl = build_ansi_hl(style, cache)
|
||||||
|
if hl then
|
||||||
|
table.insert(hls, { col, col + #chunk, hl })
|
||||||
|
end
|
||||||
|
col = col + #chunk
|
||||||
|
end
|
||||||
|
local params = {}
|
||||||
|
local seq = line:sub(s + 2, e - 1)
|
||||||
|
for part in seq:gmatch("[^;]+") do
|
||||||
|
local num = tonumber(part)
|
||||||
|
if num then
|
||||||
|
table.insert(params, num)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
apply_sgr(params)
|
||||||
|
pos = e + 1
|
||||||
|
end
|
||||||
|
clean_lines[lnum] = table.concat(clean)
|
||||||
|
highlights[lnum] = hls
|
||||||
|
end
|
||||||
|
|
||||||
|
return clean_lines, highlights
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_detail_highlights(buf, highlights)
|
||||||
|
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, detail_ns, 0, -1)
|
||||||
|
for lnum, entries in ipairs(highlights or {}) do
|
||||||
|
for _, entry in ipairs(entries or {}) do
|
||||||
|
local start_col = entry[1]
|
||||||
|
local end_col = entry[2]
|
||||||
|
local hl = entry[3]
|
||||||
|
if hl then
|
||||||
|
vim.api.nvim_buf_add_highlight(buf, detail_ns, hl, lnum - 1, start_col, end_col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_go_output_from_raw(output)
|
||||||
|
local out = {}
|
||||||
|
if not output or output == "" then
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
for line in output:gmatch("[^\n]+") do
|
||||||
|
local ok, data = pcall(vim.json.decode, line)
|
||||||
|
if ok and type(data) == "table" and data.Action == "output" and data.Test and data.Output then
|
||||||
|
if not out[data.Test] then
|
||||||
|
out[data.Test] = {}
|
||||||
|
end
|
||||||
|
local lines = vim.split(data.Output, "\n", { plain = true })
|
||||||
|
if #lines > 0 and lines[#lines] == "" then
|
||||||
|
table.remove(lines, #lines)
|
||||||
|
end
|
||||||
|
for _, item in ipairs(lines) do
|
||||||
|
table.insert(out[data.Test], item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_detail_buf(lines)
|
||||||
|
local buf = state.detail_buf
|
||||||
|
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
||||||
|
buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "bufhidden", "hide")
|
||||||
|
vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
|
||||||
|
vim.api.nvim_buf_set_option(buf, "swapfile", false)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output")
|
||||||
|
state.detail_buf = buf
|
||||||
|
vim.keymap.set("n", "<esc><esc>", function()
|
||||||
|
close_container()
|
||||||
|
end, { buffer = buf, nowait = true, silent = true })
|
||||||
|
vim.keymap.set("n", "<C-w>h", function()
|
||||||
|
M.focus_listing()
|
||||||
|
end, { buffer = buf, nowait = true, silent = true })
|
||||||
|
end
|
||||||
|
local clean_lines, highlights = parse_ansi_lines(normalize_output_lines(lines))
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, clean_lines)
|
||||||
|
apply_detail_highlights(buf, highlights)
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
local function open_detail_split(lines)
|
||||||
|
local buf = ensure_detail_buf(lines)
|
||||||
|
local base = state.last_float or {}
|
||||||
|
local width = base.width or math.floor(vim.o.columns * 0.8)
|
||||||
|
local height = base.height or math.floor(vim.o.lines * 0.8)
|
||||||
|
local row = base.row or math.floor((vim.o.lines - height) / 2)
|
||||||
|
local col = base.col or math.floor((vim.o.columns - width) / 2)
|
||||||
|
local left_width = math.max(1, math.floor(width * 0.2))
|
||||||
|
local right_width = width - left_width
|
||||||
|
if right_width < 1 then
|
||||||
|
right_width = 1
|
||||||
|
left_width = math.max(1, width - right_width)
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
||||||
|
vim.api.nvim_win_set_config(state.last_win, {
|
||||||
|
relative = "editor",
|
||||||
|
width = left_width,
|
||||||
|
height = height,
|
||||||
|
row = row,
|
||||||
|
col = col,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local right_cfg = {
|
||||||
|
relative = "editor",
|
||||||
|
width = right_width,
|
||||||
|
height = height,
|
||||||
|
row = row,
|
||||||
|
col = col + left_width,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded",
|
||||||
|
}
|
||||||
|
|
||||||
|
local right = state.detail_win
|
||||||
|
if right and vim.api.nvim_win_is_valid(right) then
|
||||||
|
vim.api.nvim_win_set_buf(right, buf)
|
||||||
|
vim.api.nvim_win_set_config(right, right_cfg)
|
||||||
|
else
|
||||||
|
state.detail_opening = true
|
||||||
|
right = vim.api.nvim_open_win(buf, true, right_cfg)
|
||||||
|
state.detail_win = right
|
||||||
|
state.detail_opening = false
|
||||||
|
end
|
||||||
|
vim.api.nvim_set_current_win(right)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.open_test_output_at_cursor()
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local line = cursor[1]
|
||||||
|
local test_name = state.last_result_line_map[line]
|
||||||
|
if not test_name then
|
||||||
|
local text = vim.api.nvim_get_current_line()
|
||||||
|
test_name = text:match("^%[%s*[%u]+%s*%]%s*%-%s*(.+)$")
|
||||||
|
end
|
||||||
|
if not test_name then
|
||||||
|
vim.notify("[test-samurai] No test output for this line", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local function resolve_output(name)
|
||||||
|
local output = state.last_test_outputs[name]
|
||||||
|
if output then
|
||||||
|
return name, output
|
||||||
|
end
|
||||||
|
local matches = {}
|
||||||
|
for full, lines in pairs(state.last_test_outputs) do
|
||||||
|
if full == name or full:sub(-#name) == name then
|
||||||
|
table.insert(matches, { name = full, lines = lines })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #matches == 1 then
|
||||||
|
return matches[1].name, matches[1].lines
|
||||||
|
end
|
||||||
|
return name, nil
|
||||||
|
end
|
||||||
|
local resolved_name, output = resolve_output(test_name)
|
||||||
|
test_name = resolved_name
|
||||||
|
if (type(output) ~= "table" or #output == 0) and state.last_raw_output and state.last_runner then
|
||||||
|
local parser = state.last_runner.parse_test_output
|
||||||
|
if type(parser) == "function" then
|
||||||
|
local ok_parse, parsed = pcall(parser, state.last_raw_output)
|
||||||
|
if ok_parse and type(parsed) == "table" then
|
||||||
|
state.last_test_outputs = parsed
|
||||||
|
test_name, output = resolve_output(test_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (type(output) ~= "table" or #output == 0) and state.last_raw_output then
|
||||||
|
local parsed = parse_go_output_from_raw(state.last_raw_output)
|
||||||
|
if type(parsed) == "table" and next(parsed) ~= nil then
|
||||||
|
state.last_test_outputs = parsed
|
||||||
|
test_name, output = resolve_output(test_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if type(output) ~= "table" or #output == 0 then
|
||||||
|
vim.notify("[test-samurai] No output captured for " .. test_name, vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
open_detail_split(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.focus_listing()
|
||||||
|
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
||||||
|
vim.api.nvim_set_current_win(state.last_win)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function run_cmd(cmd, cwd, handlers)
|
local function run_cmd(cmd, cwd, handlers)
|
||||||
local h = handlers or {}
|
local h = handlers or {}
|
||||||
|
|
||||||
@@ -383,6 +850,38 @@ local function pick_display(results, key, scope_kind)
|
|||||||
return results[key]
|
return results[key]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function track_result_lines(start_line, results, scope_kind)
|
||||||
|
if not results then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local entries = {}
|
||||||
|
local function append_kind(kind)
|
||||||
|
local display = pick_display(results, kind, scope_kind)
|
||||||
|
if type(display) ~= "table" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local full = results[kind]
|
||||||
|
for i, name in ipairs(display) do
|
||||||
|
local full_name = nil
|
||||||
|
if type(full) == "table" then
|
||||||
|
full_name = full[i]
|
||||||
|
end
|
||||||
|
if not full_name or full_name == "" then
|
||||||
|
full_name = name
|
||||||
|
end
|
||||||
|
table.insert(entries, full_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
append_kind("passes")
|
||||||
|
append_kind("skips")
|
||||||
|
append_kind("failures")
|
||||||
|
for i, name in ipairs(entries) do
|
||||||
|
if name and name ~= "" then
|
||||||
|
state.last_result_line_map[start_line + i] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function format_results(results, scope_kind)
|
local function format_results(results, scope_kind)
|
||||||
local lines = {}
|
local lines = {}
|
||||||
local passes = pick_display(results, "passes", scope_kind)
|
local passes = pick_display(results, "passes", scope_kind)
|
||||||
@@ -539,6 +1038,9 @@ end
|
|||||||
|
|
||||||
local function run_command(command, opts)
|
local function run_command(command, opts)
|
||||||
local options = opts or {}
|
local options = opts or {}
|
||||||
|
state.last_test_outputs = {}
|
||||||
|
state.last_result_line_map = {}
|
||||||
|
state.last_raw_output = nil
|
||||||
if command and type(command.cmd) == "table" and #command.cmd > 0 then
|
if command and type(command.cmd) == "table" and #command.cmd > 0 then
|
||||||
if options.save_last ~= false then
|
if options.save_last ~= false then
|
||||||
state.last_command = {
|
state.last_command = {
|
||||||
@@ -568,6 +1070,7 @@ local function run_command(command, opts)
|
|||||||
if type(parser) == "function" then
|
if type(parser) == "function" then
|
||||||
parser = { on_complete = parser }
|
parser = { on_complete = parser }
|
||||||
end
|
end
|
||||||
|
local runner = options.runner
|
||||||
local parser_state = {}
|
local parser_state = {}
|
||||||
parser_state.scope_kind = options.scope_kind
|
parser_state.scope_kind = options.scope_kind
|
||||||
local had_parsed_output = false
|
local had_parsed_output = false
|
||||||
@@ -628,6 +1131,7 @@ local function run_command(command, opts)
|
|||||||
local start_line = vim.api.nvim_buf_line_count(buf)
|
local start_line = vim.api.nvim_buf_line_count(buf)
|
||||||
append_lines(buf, lines)
|
append_lines(buf, lines)
|
||||||
apply_result_highlights(buf, start_line, lines)
|
apply_result_highlights(buf, start_line, lines)
|
||||||
|
track_result_lines(start_line, results, options.scope_kind)
|
||||||
end
|
end
|
||||||
|
|
||||||
run_cmd(cmd, cwd, {
|
run_cmd(cmd, cwd, {
|
||||||
@@ -670,8 +1174,15 @@ local function run_command(command, opts)
|
|||||||
if not buf then
|
if not buf then
|
||||||
buf = select(1, create_output_win({ header }))
|
buf = select(1, create_output_win({ header }))
|
||||||
end
|
end
|
||||||
|
local output = table.concat(output_lines, "\n")
|
||||||
|
state.last_raw_output = output
|
||||||
|
if runner and type(runner.parse_test_output) == "function" then
|
||||||
|
local ok_parse, parsed = pcall(runner.parse_test_output, output)
|
||||||
|
if ok_parse and type(parsed) == "table" then
|
||||||
|
state.last_test_outputs = parsed
|
||||||
|
end
|
||||||
|
end
|
||||||
if parser and parser.on_complete then
|
if parser and parser.on_complete then
|
||||||
local output = table.concat(output_lines, "\n")
|
|
||||||
local ok_parse, results = pcall(parser.on_complete, output, parser_state)
|
local ok_parse, results = pcall(parser.on_complete, output, parser_state)
|
||||||
if ok_parse then
|
if ok_parse then
|
||||||
handle_parsed(results)
|
handle_parsed(results)
|
||||||
|
|||||||
@@ -275,6 +275,36 @@ function runner.parse_results(output)
|
|||||||
}
|
}
|
||||||
end
|
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
|
||||||
|
|
||||||
|
function runner.parse_test_output(output)
|
||||||
|
local out = {}
|
||||||
|
if not output or output == "" then
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
for line in output:gmatch("[^\n]+") do
|
||||||
|
local ok, data = pcall(vim.json.decode, line)
|
||||||
|
if ok and type(data) == "table" and data.Action == "output" and data.Test and data.Output then
|
||||||
|
if not out[data.Test] then
|
||||||
|
out[data.Test] = {}
|
||||||
|
end
|
||||||
|
for _, item in ipairs(split_output_lines(data.Output)) do
|
||||||
|
table.insert(out[data.Test], item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
function runner.output_parser()
|
function runner.output_parser()
|
||||||
local seen_pass = {}
|
local seen_pass = {}
|
||||||
local seen_fail = {}
|
local seen_fail = {}
|
||||||
|
|||||||
@@ -452,6 +452,115 @@ function M.new(opts)
|
|||||||
return { passes = passes, failures = failures, skips = skips, display = display }
|
return { passes = passes, failures = failures, skips = skips, display = display }
|
||||||
end
|
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 function parse_jest_like(output)
|
||||||
local ok, data = pcall(vim.json.decode, output)
|
local ok, data = pcall(vim.json.decode, output)
|
||||||
if ok and type(data) == "table" then
|
if ok and type(data) == "table" then
|
||||||
@@ -648,6 +757,17 @@ function M.new(opts)
|
|||||||
return parse_output(output)
|
return parse_output(output)
|
||||||
end
|
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()
|
function runner.output_parser()
|
||||||
local state = { raw = {}, done = false, saw_stream = false }
|
local state = { raw = {}, done = false, saw_stream = false }
|
||||||
local failures = {}
|
local failures = {}
|
||||||
|
|||||||
@@ -1400,3 +1400,430 @@ describe("test-samurai output formatting", function()
|
|||||||
assert.is_true(has_fail)
|
assert.is_true(has_fail)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("test-samurai output detail view", function()
|
||||||
|
before_each(function()
|
||||||
|
test_samurai.setup()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local function find_float_wins()
|
||||||
|
local wins = vim.api.nvim_tabpage_list_wins(0)
|
||||||
|
local out = {}
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
local cfg = vim.api.nvim_win_get_config(win)
|
||||||
|
if cfg.relative ~= "" then
|
||||||
|
table.insert(out, win)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_non_float_win()
|
||||||
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
local cfg = vim.api.nvim_win_get_config(win)
|
||||||
|
if cfg.relative == "" then
|
||||||
|
return win
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it("opens failing test output in a right vsplit", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = "=== RUN TestFoo/Sub\n" }),
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = " foo_test.go:10: expected\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/Sub" }),
|
||||||
|
}, 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_detail_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
local wins = find_float_wins()
|
||||||
|
assert.equals(2, #wins)
|
||||||
|
local right = nil
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
if win ~= output_win then
|
||||||
|
right = win
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(right)
|
||||||
|
local left_cfg = vim.api.nvim_win_get_config(output_win)
|
||||||
|
local right_cfg = vim.api.nvim_win_get_config(right)
|
||||||
|
assert.equals(left_cfg.row, right_cfg.row)
|
||||||
|
assert.equals(left_cfg.height, right_cfg.height)
|
||||||
|
assert.equals(left_cfg.col + left_cfg.width, right_cfg.col)
|
||||||
|
local total_width = left_cfg.width + right_cfg.width
|
||||||
|
local expected_left = math.floor(total_width * 0.2)
|
||||||
|
assert.is_true(math.abs(left_cfg.width - expected_left) <= 1)
|
||||||
|
local right_buf = vim.api.nvim_win_get_buf(right)
|
||||||
|
local detail = vim.api.nvim_buf_get_lines(right_buf, 0, -1, false)
|
||||||
|
assert.are.same({
|
||||||
|
"=== RUN TestFoo/Sub",
|
||||||
|
" foo_test.go:10: expected",
|
||||||
|
}, detail)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("reuses an existing right split for test output", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = "=== RUN TestFoo/Sub\n" }),
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = " foo_test.go:10: expected\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/Sub" }),
|
||||||
|
}, 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_detail_reuse_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
local wins = find_float_wins()
|
||||||
|
local right = nil
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
if win ~= output_win then
|
||||||
|
right = win
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(right)
|
||||||
|
local right_buf = vim.api.nvim_win_get_buf(right)
|
||||||
|
vim.api.nvim_buf_set_lines(right_buf, 0, -1, false, { "old output" })
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_win(output_win)
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
local updated = find_float_wins()
|
||||||
|
local updated_right = nil
|
||||||
|
for _, win in ipairs(updated) do
|
||||||
|
if win ~= output_win then
|
||||||
|
updated_right = win
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.equals(right, updated_right)
|
||||||
|
local detail = vim.api.nvim_buf_get_lines(right_buf, 0, -1, false)
|
||||||
|
assert.are.same({
|
||||||
|
"=== RUN TestFoo/Sub",
|
||||||
|
" foo_test.go:10: expected",
|
||||||
|
}, detail)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("translates ANSI codes into highlights for detail output", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestAnsi/Sub", Output = "\27[31mFAIL\27[0m bad\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestAnsi/Sub" }),
|
||||||
|
}, 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_detail_ansi_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
local wins = find_float_wins()
|
||||||
|
local right = nil
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
if win ~= output_win then
|
||||||
|
right = win
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(right)
|
||||||
|
local right_buf = vim.api.nvim_win_get_buf(right)
|
||||||
|
local detail = vim.api.nvim_buf_get_lines(right_buf, 0, -1, false)
|
||||||
|
assert.are.same({ "FAIL bad" }, detail)
|
||||||
|
|
||||||
|
local ns = vim.api.nvim_get_namespaces()["TestSamuraiDetailAnsi"]
|
||||||
|
assert.is_not_nil(ns)
|
||||||
|
local marks = vim.api.nvim_buf_get_extmarks(right_buf, ns, 0, -1, { details = true })
|
||||||
|
assert.is_true(#marks > 0)
|
||||||
|
assert.is_not_nil(marks[1][4].hl_group)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("closes detail float and restores listing width", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = "=== RUN TestFoo/Sub\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/Sub" }),
|
||||||
|
}, 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_detail_close_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
local wins = find_float_wins()
|
||||||
|
assert.equals(2, #wins)
|
||||||
|
local right = nil
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
if win ~= output_win then
|
||||||
|
right = win
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(right)
|
||||||
|
local left_cfg = vim.api.nvim_win_get_config(output_win)
|
||||||
|
local right_cfg = vim.api.nvim_win_get_config(right)
|
||||||
|
local total_width = left_cfg.width + right_cfg.width
|
||||||
|
|
||||||
|
vim.api.nvim_win_close(right, true)
|
||||||
|
|
||||||
|
local remaining = find_float_wins()
|
||||||
|
assert.equals(1, #remaining)
|
||||||
|
local cfg = vim.api.nvim_win_get_config(remaining[1])
|
||||||
|
assert.equals(total_width, cfg.width)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("closes container when listing float is closed", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = "=== RUN TestFoo/Sub\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/Sub" }),
|
||||||
|
}, 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_detail_close_listing_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
vim.api.nvim_win_close(output_win, true)
|
||||||
|
|
||||||
|
local remaining = find_float_wins()
|
||||||
|
assert.equals(0, #remaining)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("closes container when navigating out of floats", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = "=== RUN TestFoo/Sub\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/Sub" }),
|
||||||
|
}, 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_detail_nav_close_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
local non_float = find_non_float_win()
|
||||||
|
assert.is_not_nil(non_float)
|
||||||
|
vim.api.nvim_set_current_win(non_float)
|
||||||
|
|
||||||
|
local remaining = find_float_wins()
|
||||||
|
assert.equals(0, #remaining)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("keeps container open when focusing listing from detail", function()
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, {
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/Sub", Output = "=== RUN TestFoo/Sub\n" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/Sub" }),
|
||||||
|
}, 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_detail_focus_listing_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
test_samurai.test_file()
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
local target = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match("^%[ FAIL %] %- ") then
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(target)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target, 0 })
|
||||||
|
|
||||||
|
local output_win = vim.api.nvim_get_current_win()
|
||||||
|
core.open_test_output_at_cursor()
|
||||||
|
|
||||||
|
local wins = find_float_wins()
|
||||||
|
local detail_win = nil
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
if win ~= output_win then
|
||||||
|
detail_win = win
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(detail_win)
|
||||||
|
vim.api.nvim_set_current_win(detail_win)
|
||||||
|
|
||||||
|
core.focus_listing()
|
||||||
|
|
||||||
|
assert.equals(output_win, vim.api.nvim_get_current_win())
|
||||||
|
local remaining = find_float_wins()
|
||||||
|
assert.equals(2, #remaining)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user