diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index c86833e..000a2a4 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -14,9 +14,47 @@ local state = { last_scope_kind = nil, last_scope_exit_code = nil, last_scope_failures = nil, + last_border_kind = "default", autocmds_set = false, } +local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary") +local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult") + +local function get_hl_fg(name) + local ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = name, link = true }) + if ok and type(hl) == "table" and hl.fg then + return hl.fg + end + return nil +end + +local function resolve_hl_fg(names, fallback) + for _, name in ipairs(names or {}) do + local fg = get_hl_fg(name) + if fg then + return fg + end + end + return fallback +end + +local function setup_summary_highlights() + local normal_fg = get_hl_fg("Normal") + local pass_fg = resolve_hl_fg({ "DiffAdd", "DiagnosticOk" }, normal_fg) + local fail_fg = resolve_hl_fg({ "DiffDelete", "DiagnosticError" }, normal_fg) + local skip_fg = resolve_hl_fg({ "DiagnosticInfo", "DiagnosticHint" }, normal_fg) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummaryBold", { fg = normal_fg, bold = true }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummaryPass", { fg = pass_fg, bold = true }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummaryFail", { fg = fail_fg, bold = true }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummarySkip", { fg = skip_fg, bold = true }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultPass", { fg = pass_fg }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultFail", { fg = fail_fg }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultSkip", { fg = skip_fg }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderPass", { fg = pass_fg, bold = true }) + pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderFail", { fg = fail_fg, bold = true }) +end + local function load_runners() state.runners = {} local opts = config.get() @@ -51,12 +89,20 @@ local function ensure_output_autocmds() end, }) + vim.api.nvim_create_autocmd("ColorScheme", { + group = group, + callback = function() + setup_summary_highlights() + end, + }) + state.autocmds_set = true end function M.setup() load_runners() ensure_output_autocmds() + setup_summary_highlights() state.last_command = nil state.last_runner = nil state.last_scope_command = nil @@ -64,6 +110,7 @@ function M.setup() state.last_scope_kind = nil state.last_scope_exit_code = nil state.last_scope_failures = nil + state.last_border_kind = "default" end function M.reload_runners() @@ -176,6 +223,19 @@ local function float_geometry() return width, height, row, col end +local function apply_border_kind(win, kind) + if not (win and vim.api.nvim_win_is_valid(win)) then + return + end + if kind == "pass" then + vim.api.nvim_win_set_option(win, "winhighlight", "FloatBorder:TestSamuraiBorderPass") + elseif kind == "fail" then + vim.api.nvim_win_set_option(win, "winhighlight", "FloatBorder:TestSamuraiBorderFail") + else + vim.api.nvim_win_set_option(win, "winhighlight", "") + end +end + local function create_output_win(initial_lines) 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) @@ -215,6 +275,7 @@ local function create_output_win(initial_lines) state.last_win = win state.last_buf = buf + apply_border_kind(win, state.last_border_kind) return buf, win end @@ -251,6 +312,7 @@ local function reopen_output_win() end, { buffer = state.last_buf, nowait = true, silent = true }) state.last_win = win + apply_border_kind(win, state.last_border_kind) return state.last_buf, win end @@ -344,6 +406,137 @@ local function format_results(results, scope_kind) return lines end +local function make_summary_tracker(enabled) + if not enabled then + return nil + end + return { + passes = {}, + failures = {}, + skips = {}, + } +end + +local function add_unique_items(target, items) + if type(items) ~= "table" then + return + end + for _, item in ipairs(items) do + if item and item ~= "" then + target[item] = true + end + end +end + +local function update_summary(summary, results) + if not summary or not results then + return + end + add_unique_items(summary.passes, results.passes) + add_unique_items(summary.failures, results.failures) + add_unique_items(summary.skips, results.skips) +end + +local function count_summary(summary) + if not summary then + return 0, 0, 0, 0 + end + local function count(set) + local total = 0 + for _ in pairs(set) do + total = total + 1 + end + return total + end + local pass_count = count(summary.passes) + local fail_count = count(summary.failures) + local skip_count = count(summary.skips) + return pass_count, fail_count, skip_count, pass_count + fail_count + skip_count +end + +local function format_summary_lines(summary, elapsed_seconds) + local pass_count, fail_count, skip_count, total = count_summary(summary) + local minutes = 0 + local seconds = 0 + if type(elapsed_seconds) == "number" and elapsed_seconds >= 0 then + local total_seconds = math.floor(elapsed_seconds + 0.0000001) + minutes = math.floor(total_seconds / 60) + seconds = total_seconds % 60 + end + local duration = string.format("%02dm %02ds", minutes, seconds) + return { + "", + string.format("TOTAL %d", total), + string.format("DURATION %s", duration), + "", + string.format(" PASS %d - SKIPPED %d - FAILED %d", pass_count, skip_count, fail_count), + } +end + +local function highlight_label_number(buf, ns, lnum, line, label, hl_group) + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + local label_start = line:find(label, 1, true) + if not label_start then + return + end + local num_start, num_end = line:find("%d+", label_start + #label) + if not num_start then + return + end + vim.api.nvim_buf_add_highlight(buf, ns, hl_group, lnum, num_start - 1, num_end) +end + +local function highlight_label_word(buf, ns, lnum, line, label, hl_group) + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + local label_start = line:find(label, 1, true) + if not label_start then + return + end + vim.api.nvim_buf_add_highlight(buf, ns, hl_group, lnum, label_start - 1, label_start - 1 + #label) +end + +local function apply_summary_highlights(buf, start_line, lines) + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + for i, line in ipairs(lines or {}) do + local lnum = start_line + i - 1 + if line:match("^%s*PASS%s+%d+") then + highlight_label_word(buf, summary_ns, lnum, line, "PASS", "TestSamuraiSummaryPass") + highlight_label_number(buf, summary_ns, lnum, line, "PASS", "TestSamuraiSummaryPass") + highlight_label_word(buf, summary_ns, lnum, line, "SKIPPED", "TestSamuraiSummarySkip") + highlight_label_number(buf, summary_ns, lnum, line, "SKIPPED", "TestSamuraiSummarySkip") + highlight_label_word(buf, summary_ns, lnum, line, "FAILED", "TestSamuraiSummaryFail") + highlight_label_number(buf, summary_ns, lnum, line, "FAILED", "TestSamuraiSummaryFail") + elseif line:match("^TOTAL%s+%d+") then + highlight_label_word(buf, summary_ns, lnum, line, "TOTAL", "TestSamuraiSummaryBold") + highlight_label_number(buf, summary_ns, lnum, line, "TOTAL", "TestSamuraiSummaryBold") + elseif line:match("^DURATION%s+") then + highlight_label_word(buf, summary_ns, lnum, line, "DURATION", "TestSamuraiSummaryBold") + end + end +end + +local function apply_result_highlights(buf, start_line, lines) + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + for i, line in ipairs(lines or {}) do + local lnum = start_line + i - 1 + if line:match("^%[ PASS %]") then + highlight_label_word(buf, result_ns, lnum, line, "PASS", "TestSamuraiResultPass") + elseif line:match("^%[ FAIL %]") then + highlight_label_word(buf, result_ns, lnum, line, "FAIL", "TestSamuraiResultFail") + elseif line:match("^%[ SKIP %]") then + highlight_label_word(buf, result_ns, lnum, line, "SKIP", "TestSamuraiResultSkip") + end + end +end + local function run_command(command, opts) local options = opts or {} if command and type(command.cmd) == "table" and #command.cmd > 0 then @@ -378,6 +571,14 @@ local function run_command(command, opts) local parser_state = {} parser_state.scope_kind = options.scope_kind local had_parsed_output = false + local summary_enabled = options.scope_kind == "file" or options.scope_kind == "all" or options.scope_kind == "nearest" + local summary = make_summary_tracker(summary_enabled) + local result_counts = make_summary_tracker(true) + state.last_border_kind = "default" + local start_time = nil + if vim.loop and vim.loop.hrtime then + start_time = vim.loop.hrtime() + end local output_lines = {} @@ -410,6 +611,8 @@ local function run_command(command, opts) return end had_parsed_output = true + update_summary(summary, results) + update_summary(result_counts, results) if options.track_scope then if results.failures_all ~= nil then state.last_scope_failures = results.failures_all @@ -422,7 +625,9 @@ local function run_command(command, opts) return end ensure_output_started() + local start_line = vim.api.nvim_buf_line_count(buf) append_lines(buf, lines) + apply_result_highlights(buf, start_line, lines) end run_cmd(cmd, cwd, { @@ -472,6 +677,15 @@ local function run_command(command, opts) handle_parsed(results) end end + local pass_count, fail_count = count_summary(result_counts) + if fail_count > 0 then + state.last_border_kind = "fail" + elseif pass_count > 0 then + state.last_border_kind = "pass" + else + state.last_border_kind = "default" + end + apply_border_kind(state.last_win, state.last_border_kind) if parser then if not had_parsed_output and #output_lines > 0 then ensure_output_started() @@ -479,6 +693,17 @@ local function run_command(command, opts) elseif not has_output then ensure_output_started() end + if summary_enabled then + local elapsed = nil + if start_time and vim.loop and vim.loop.hrtime then + elapsed = (vim.loop.hrtime() - start_time) / 1000000000 + end + ensure_output_started() + local summary_lines = format_summary_lines(summary, elapsed) + local start_line = vim.api.nvim_buf_line_count(buf) + append_lines(buf, summary_lines) + apply_summary_highlights(buf, start_line, summary_lines) + end append_lines(buf, { "", "[exit code] " .. tostring(code) }) else if not has_output then @@ -487,6 +712,17 @@ local function run_command(command, opts) vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {}) end end + if summary_enabled then + local elapsed = nil + if start_time and vim.loop and vim.loop.hrtime then + elapsed = (vim.loop.hrtime() - start_time) / 1000000000 + end + ensure_output_started() + local summary_lines = format_summary_lines(summary, elapsed) + local start_line = vim.api.nvim_buf_line_count(buf) + append_lines(buf, summary_lines) + apply_summary_highlights(buf, start_line, summary_lines) + end append_lines(buf, { "", "[exit code] " .. tostring(code) }) end if options.track_scope then diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index 370ca48..612a61d 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -164,6 +164,161 @@ describe("test-samurai output formatting", function() assert.is_true(has_fail) end) + it("applies PASS/FAIL/SKIP highlights to parsed output", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + { status = "skipped", title = "inner skip", fullName = "outer inner skip" }, + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, 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_highlight_parsed.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 7, 0 }) + + core.run_nearest() + + local out_buf = vim.api.nvim_get_current_buf() + local ns = vim.api.nvim_create_namespace("TestSamuraiResult") + local extmarks = vim.api.nvim_buf_get_extmarks(out_buf, ns, 0, -1, { details = true }) + + vim.fn.jobstart = orig_jobstart + + local groups = {} + for _, mark in ipairs(extmarks) do + local details = mark[4] or {} + if details.hl_group then + groups[details.hl_group] = true + end + end + + assert.is_true(groups.TestSamuraiResultPass) + assert.is_true(groups.TestSamuraiResultFail) + assert.is_true(groups.TestSamuraiResultSkip) + end) + + it("sets pass border when there are only passing tests", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_border_pass.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_nearest() + + local win = vim.api.nvim_get_current_win() + local hl = vim.api.nvim_win_get_option(win, "winhighlight") + + vim.fn.jobstart = orig_jobstart + + assert.equals("FloatBorder:TestSamuraiBorderPass", hl) + end) + + it("sets fail border when there are failing tests", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, 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_border_fail.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_nearest() + + local win = vim.api.nvim_get_current_win() + local hl = vim.api.nvim_win_get_option(win, "winhighlight") + + vim.fn.jobstart = orig_jobstart + + assert.equals("FloatBorder:TestSamuraiBorderFail", hl) + end) + it("does not print raw JSON output for jest runs", function() local json = vim.json.encode({ testResults = { @@ -334,7 +489,7 @@ describe("test-samurai output formatting", function() has_skip = true elseif line == "[ FAIL ] - inner 2" then has_fail = true - elseif line:match("^%s*PASS%s+") then + elseif line:match("^%s*PASS%s+") and line ~= " PASS 1 - SKIPPED 1 - FAILED 1" then has_raw_verbose = true end end @@ -468,7 +623,7 @@ describe("test-samurai output formatting", function() has_skip = true elseif line == "[ FAIL ] - inner 2" then has_fail = true - elseif line:match("^%s*PASS%s+") then + elseif line:match("^%s*PASS%s+") and line ~= " PASS 1 - SKIPPED 1 - FAILED 1" then has_raw_verbose = true end end @@ -479,6 +634,365 @@ describe("test-samurai output formatting", function() assert.is_false(has_raw_verbose) end) + it("adds a summary line for TSamFile output", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + { status = "skipped", title = "inner skip", fullName = "outer inner skip" }, + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local orig_hrtime = vim.loop.hrtime + local hr_calls = 0 + vim.loop.hrtime = function() + hr_calls = hr_calls + 1 + if hr_calls == 1 then + return 1000000000 + end + return 3500000000 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_summary_file.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 7, 0 }) + + core.run_file() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + vim.loop.hrtime = orig_hrtime + + local has_total = false + local has_breakdown = false + local has_duration = false + local saw_blank = false + for _, line in ipairs(lines) do + if line == "" then + saw_blank = true + elseif line == "TOTAL 3" then + has_total = true + elseif line == "DURATION 00m 02s" then + has_duration = true + elseif line == " PASS 1 - SKIPPED 1 - FAILED 1" then + has_breakdown = true + end + end + + assert.is_true(saw_blank) + assert.is_true(has_total) + assert.is_true(has_breakdown) + assert.is_true(has_duration) + end) + + it("adds a summary line for TSamAll output", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + { status = "skipped", title = "inner skip", fullName = "outer inner skip" }, + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local orig_hrtime = vim.loop.hrtime + local hr_calls = 0 + vim.loop.hrtime = function() + hr_calls = hr_calls + 1 + if hr_calls == 1 then + return 500000000 + end + return 2000000000 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_summary_all.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 7, 0 }) + + core.run_all() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + vim.loop.hrtime = orig_hrtime + + local has_total = false + local has_breakdown = false + local has_duration = false + local saw_blank = false + for _, line in ipairs(lines) do + if line == "" then + saw_blank = true + elseif line == "TOTAL 3" then + has_total = true + elseif line == "DURATION 00m 01s" then + has_duration = true + elseif line == " PASS 1 - SKIPPED 1 - FAILED 1" then + has_breakdown = true + end + end + + assert.is_true(saw_blank) + assert.is_true(has_total) + assert.is_true(has_breakdown) + assert.is_true(has_duration) + end) + + it("adds a summary line for TSamNearest output", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + { status = "skipped", title = "inner skip", fullName = "outer inner skip" }, + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local orig_hrtime = vim.loop.hrtime + local hr_calls = 0 + vim.loop.hrtime = function() + hr_calls = hr_calls + 1 + if hr_calls == 1 then + return 2000000000 + end + return 4500000000 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_summary_nearest.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_nearest() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + vim.loop.hrtime = orig_hrtime + + local has_total = false + local has_breakdown = false + local has_duration = false + local saw_blank = false + for _, line in ipairs(lines) do + if line == "" then + saw_blank = true + elseif line == "TOTAL 3" then + has_total = true + elseif line == "DURATION 00m 02s" then + has_duration = true + elseif line == " PASS 1 - SKIPPED 1 - FAILED 1" then + has_breakdown = true + end + end + + assert.is_true(saw_blank) + assert.is_true(has_total) + assert.is_true(has_breakdown) + assert.is_true(has_duration) + end) + + it("applies summary highlights for TSamAll output", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + { status = "skipped", title = "inner skip", fullName = "outer inner skip" }, + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local orig_hrtime = vim.loop.hrtime + local hr_calls = 0 + vim.loop.hrtime = function() + hr_calls = hr_calls + 1 + if hr_calls == 1 then + return 1000000000 + end + return 2500000000 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_summary_highlight_all.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 7, 0 }) + + core.run_all() + + local out_buf = vim.api.nvim_get_current_buf() + local ns = vim.api.nvim_create_namespace("TestSamuraiSummary") + local extmarks = vim.api.nvim_buf_get_extmarks(out_buf, ns, 0, -1, { details = true }) + + vim.fn.jobstart = orig_jobstart + vim.loop.hrtime = orig_hrtime + + local groups = {} + for _, mark in ipairs(extmarks) do + local details = mark[4] or {} + if details.hl_group then + groups[details.hl_group] = true + end + end + + assert.is_true(groups.TestSamuraiSummaryBold) + assert.is_true(groups.TestSamuraiSummaryPass) + assert.is_true(groups.TestSamuraiSummaryFail) + assert.is_true(groups.TestSamuraiSummarySkip) + end) + + it("derives summary highlight colors from diagnostic fallbacks", function() + local orig_get_hl = vim.api.nvim_get_hl + local orig_set_hl = vim.api.nvim_set_hl + local set_calls = {} + + vim.api.nvim_get_hl = function(_ns, opts) + if opts.name == "DiffAdd" or opts.name == "DiffDelete" then + return {} + end + if opts.name == "DiagnosticOk" then + return { fg = 111 } + end + if opts.name == "DiagnosticError" then + return { fg = 222 } + end + if opts.name == "DiagnosticInfo" then + return { fg = 333 } + end + if opts.name == "Normal" then + return { fg = 99 } + end + return {} + end + + vim.api.nvim_set_hl = function(_ns, name, opts) + set_calls[name] = opts + end + + core.setup() + + vim.api.nvim_get_hl = orig_get_hl + vim.api.nvim_set_hl = orig_set_hl + + assert.is_true(set_calls.TestSamuraiSummaryPass.fg == 111) + assert.is_true(set_calls.TestSamuraiSummaryFail.fg == 222) + assert.is_true(set_calls.TestSamuraiSummarySkip.fg == 333) + end) + it("formats go subtests as short names", function() local json_line = vim.json.encode({ Action = "pass",