diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 5836d4e..942d2bb 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -99,6 +99,144 @@ local function handle_ctrl_l_in_listing() M.focus_detail() end +local function is_fail_listing_line(line) + return line and line:match("^%[ FAIL %] %- ") ~= nil +end + +local function jump_listing_fail(direction) + local win = vim.api.nvim_get_current_win() + local buf = vim.api.nvim_get_current_buf() + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return + end + local total = vim.api.nvim_buf_line_count(buf) + if total == 0 then + return + end + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local cursor = vim.api.nvim_win_get_cursor(win) + local start = cursor[1] + + local function scan_forward(from, to) + if from < 1 then + from = 1 + end + if to > total then + to = total + end + for i = from, to do + if is_fail_listing_line(lines[i]) then + return i + end + end + return nil + end + + local function scan_backward(from, to) + if from < 1 then + from = 1 + end + if to > total then + to = total + end + for i = from, to, -1 do + if is_fail_listing_line(lines[i]) then + return i + end + end + return nil + end + + local target = nil + if direction == "prev" then + target = scan_backward(start - 1, 1) + if not target then + target = scan_backward(total, start) + end + else + target = scan_forward(start + 1, total) + if not target then + target = scan_forward(1, start) + end + end + + if target then + vim.api.nvim_win_set_cursor(win, { target, 0 }) + end +end + +local function find_normal_window() + 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 + +local function jump_to_listing_test() + local cursor = vim.api.nvim_win_get_cursor(0) + local line = cursor[1] + local text = vim.api.nvim_get_current_line() + local status = text:match("^%[%s*(%u+)%s*%]%s*%-") + if status ~= "PASS" and status ~= "FAIL" and status ~= "SKIP" then + return + end + local test_name = state.last_result_line_map[line] + if not test_name or test_name == "" then + test_name = text:match("^%[%s*[%u]+%s*%]%s*%-%s*(.+)$") + end + if not test_name or test_name == "" then + return + end + local runner = state.last_scope_runner or state.last_runner + if not runner or type(runner.collect_failed_locations) ~= "function" then + return + end + local command = state.last_scope_command or state.last_command + if not command then + return + end + local ok, items = pcall(runner.collect_failed_locations, { test_name }, command, state.last_scope_kind) + if not ok or type(items) ~= "table" or #items == 0 then + return + end + local target = items[1] + if not target or not target.filename or target.filename == "" then + return + end + + local target_win = find_normal_window() + close_container() + if not (target_win and vim.api.nvim_win_is_valid(target_win)) then + target_win = find_normal_window() + end + local buf = vim.fn.bufadd(target.filename) + vim.fn.bufload(buf) + if target_win and vim.api.nvim_win_is_valid(target_win) then + vim.api.nvim_win_set_buf(target_win, buf) + vim.api.nvim_set_current_win(target_win) + else + vim.api.nvim_set_current_buf(buf) + end + local total = vim.api.nvim_buf_line_count(buf) + if total < 1 then + return + end + local lnum = target.lnum or 1 + if lnum < 1 then + lnum = 1 + elseif lnum > total then + lnum = total + end + local col = target.col or 1 + if col < 1 then + col = 1 + end + vim.api.nvim_win_set_cursor(0, { lnum, col - 1 }) +end + 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 @@ -490,6 +628,15 @@ local function create_output_win(initial_lines) vim.keymap.set("n", "z", function() M.toggle_detail_full() end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "nf", function() + jump_listing_fail("next") + end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "pf", function() + jump_listing_fail("prev") + end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "o", function() + jump_to_listing_test() + end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "qn", function() jump_to_first_quickfix() end, { buffer = buf, nowait = true, silent = true }) @@ -545,6 +692,15 @@ local function reopen_output_win() vim.keymap.set("n", "z", function() M.toggle_detail_full() end, { buffer = state.last_buf, nowait = true, silent = true }) + vim.keymap.set("n", "nf", function() + jump_listing_fail("next") + end, { buffer = state.last_buf, nowait = true, silent = true }) + vim.keymap.set("n", "pf", function() + jump_listing_fail("prev") + end, { buffer = state.last_buf, nowait = true, silent = true }) + vim.keymap.set("n", "o", function() + jump_to_listing_test() + end, { buffer = state.last_buf, nowait = true, silent = true }) vim.keymap.set("n", "qn", function() jump_to_first_quickfix() end, { buffer = state.last_buf, nowait = true, silent = true }) diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index f388aba..c9b636b 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -149,6 +149,212 @@ describe("test-samurai public API", function() end) end) +describe("test-samurai output listing fail navigation", function() + before_each(function() + test_samurai.setup() + end) + + after_each(function() + close_output_container() + end) + + local function find_listing_win() + local current = vim.api.nvim_get_current_win() + local cfg = vim.api.nvim_win_get_config(current) + if cfg.relative ~= "" then + return current + end + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local win_cfg = vim.api.nvim_win_get_config(win) + if win_cfg.relative ~= "" then + return win + end + end + return nil + end + + local function collect_fail_lines(buf) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local out = {} + for i, line in ipairs(lines) do + if line:match("^%[ FAIL %] %- ") then + table.insert(out, i) + end + end + return out + end + + local function press(keys) + local mapped = vim.api.nvim_replace_termcodes(keys, true, false, true) + vim.api.nvim_feedkeys(mapped, "x", false) + vim.wait(20) + end + + it("jumps to the next fail and wraps to the first", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "failed", title = "inner 1", fullName = "outer inner 1" }, + { status = "passed", title = "inner 2", fullName = "outer inner 2" }, + { status = "failed", title = "inner 3", fullName = "outer inner 3" }, + }, + }, + }, + }) + + 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_listing_next_fail.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " })", + ' it("inner 2", function() {', + " })", + ' it("inner 3", function() {', + " })", + "})", + }) + + core.run_nearest() + + local listing = find_listing_win() + assert.is_not_nil(listing) + vim.api.nvim_set_current_win(listing) + local listing_buf = vim.api.nvim_win_get_buf(listing) + local fails = collect_fail_lines(listing_buf) + assert.equals(2, #fails) + + vim.api.nvim_win_set_cursor(listing, { fails[1], 0 }) + press("nf") + assert.equals(fails[2], vim.api.nvim_win_get_cursor(listing)[1]) + press("nf") + assert.equals(fails[1], vim.api.nvim_win_get_cursor(listing)[1]) + + vim.fn.jobstart = orig_jobstart + end) + + it("jumps to the previous fail and wraps to the last", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "failed", title = "inner 1", fullName = "outer inner 1" }, + { status = "passed", title = "inner 2", fullName = "outer inner 2" }, + { status = "failed", title = "inner 3", fullName = "outer inner 3" }, + }, + }, + }, + }) + + 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_listing_prev_fail.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " })", + ' it("inner 2", function() {', + " })", + ' it("inner 3", function() {', + " })", + "})", + }) + + core.run_nearest() + + local listing = find_listing_win() + assert.is_not_nil(listing) + vim.api.nvim_set_current_win(listing) + local listing_buf = vim.api.nvim_win_get_buf(listing) + local fails = collect_fail_lines(listing_buf) + assert.equals(2, #fails) + + vim.api.nvim_win_set_cursor(listing, { fails[2], 0 }) + press("pf") + assert.equals(fails[1], vim.api.nvim_win_get_cursor(listing)[1]) + press("pf") + assert.equals(fails[2], vim.api.nvim_win_get_cursor(listing)[1]) + + vim.fn.jobstart = orig_jobstart + end) + + it("does nothing when there are no fail lines", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + { status = "skipped", 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 bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_listing_no_fail.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " })", + ' it("inner 2", function() {', + " })", + "})", + }) + + core.run_nearest() + + local listing = find_listing_win() + assert.is_not_nil(listing) + vim.api.nvim_set_current_win(listing) + local before = vim.api.nvim_win_get_cursor(listing)[1] + press("nf") + assert.equals(before, vim.api.nvim_win_get_cursor(listing)[1]) + press("pf") + assert.equals(before, vim.api.nvim_win_get_cursor(listing)[1]) + + vim.fn.jobstart = orig_jobstart + end) +end) + describe("test-samurai output formatting", function() before_each(function() test_samurai.setup() @@ -2258,6 +2464,203 @@ describe("test-samurai output detail view", function() vim.fn.jobstart = orig_jobstart end) + it("schliesst den Test-Container und springt mit o zum Test der Listing-Zeile", 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 = "pass", Test = "TestFoo" }), + }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_jump") + vim.fn.mkdir(root, "p") + local target = root .. "/output_detail_o_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestFoo(t *testing.T) {", + "}", + }, target) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, target) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestFoo(t *testing.T) {", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + + test_samurai.test_file() + + local wins = find_float_wins() + assert.equals(1, #wins) + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + local target_line = nil + for i, line in ipairs(lines) do + if line:match("^%[ PASS %] %- ") then + target_line = i + break + end + end + assert.is_not_nil(target_line) + vim.api.nvim_win_set_cursor(0, { target_line, 0 }) + + local keys = vim.api.nvim_replace_termcodes("o", true, false, true) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20, function() + return #find_float_wins() == 0 + end) + + assert.equals(target, vim.api.nvim_buf_get_name(0)) + local cursor = vim.api.nvim_win_get_cursor(0) + assert.equals(3, cursor[1]) + + vim.fn.jobstart = orig_jobstart + end) + + it("ignoriert o auf Nicht-Ergebniszeilen im Listing", 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 = "pass", Test = "TestFoo" }), + }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_jump") + vim.fn.mkdir(root, "p") + local target = root .. "/output_detail_o_ignore_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestFoo(t *testing.T) {", + "}", + }, target) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, target) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestFoo(t *testing.T) {", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + + test_samurai.test_file() + + local wins = find_float_wins() + assert.equals(1, #wins) + + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + + local keys = vim.api.nvim_replace_termcodes("o", true, false, true) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20) + + local remaining = find_float_wins() + assert.equals(1, #remaining) + assert.is_true(vim.api.nvim_buf_get_name(0) ~= target) + + vim.fn.jobstart = orig_jobstart + end) + + it("oeffnet mit o den nicht geladenen Buffer fuer Mocha-TSamAll", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.js-mocha", + }, + }) + + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_mocha_jump") + local test_dir = root .. "/test" + vim.fn.mkdir(test_dir, "p") + vim.fn.writefile({ '{ "name": "mocha-jump" }' }, root .. "/package.json") + + local file1 = test_dir .. "/one.test.js" + local file2 = test_dir .. "/two.test.js" + vim.fn.writefile({ + 'describe("suite one", function() {', + ' it("test one", function() {', + " })", + "})", + }, file1) + vim.fn.writefile({ + 'describe("suite two", function() {', + ' it("test two", function() {', + " })", + "})", + }, file2) + + 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({ event = "pass", fullTitle = "suite one test one", title = "test one" }), + vim.json.encode({ event = "pass", fullTitle = "suite two test two", title = "test two" }), + }, 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, file1) + vim.bo[bufnr].filetype = "javascript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("suite one", function() {', + ' it("test one", function() {', + " })", + "})", + }) + vim.api.nvim_set_current_buf(bufnr) + + test_samurai.test_all() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + local target_line = nil + for i, line in ipairs(lines) do + if line == "[ PASS ] - suite two test two" then + target_line = i + break + end + end + assert.is_not_nil(target_line) + vim.api.nvim_win_set_cursor(0, { target_line, 0 }) + + local keys = vim.api.nvim_replace_termcodes("o", true, false, true) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20, function() + return #find_float_wins() == 0 + end) + + assert.equals(file2, vim.api.nvim_buf_get_name(0)) + local cursor = vim.api.nvim_win_get_cursor(0) + assert.equals(2, cursor[1]) + + vim.fn.jobstart = orig_jobstart + end) + it("disables hardtime in listing/detail and restores on close", function() local orig_hardtime = package.loaded["hardtime"] local disable_calls = 0 diff --git a/tests/tmp_mocha_jump/package.json b/tests/tmp_mocha_jump/package.json new file mode 100644 index 0000000..4dcce0b --- /dev/null +++ b/tests/tmp_mocha_jump/package.json @@ -0,0 +1 @@ +{ "name": "mocha-jump" } diff --git a/tests/tmp_mocha_jump/test/one.test.js b/tests/tmp_mocha_jump/test/one.test.js new file mode 100644 index 0000000..74d6755 --- /dev/null +++ b/tests/tmp_mocha_jump/test/one.test.js @@ -0,0 +1,4 @@ +describe("suite one", function() { + it("test one", function() { + }) +}) diff --git a/tests/tmp_mocha_jump/test/two.test.js b/tests/tmp_mocha_jump/test/two.test.js new file mode 100644 index 0000000..4c378d0 --- /dev/null +++ b/tests/tmp_mocha_jump/test/two.test.js @@ -0,0 +1,4 @@ +describe("suite two", function() { + it("test two", function() { + }) +}) diff --git a/tests/tmp_qf_jump/output_detail_o_ignore_test.go b/tests/tmp_qf_jump/output_detail_o_ignore_test.go new file mode 100644 index 0000000..cddd917 --- /dev/null +++ b/tests/tmp_qf_jump/output_detail_o_ignore_test.go @@ -0,0 +1,4 @@ +package foo + +func TestFoo(t *testing.T) { +} diff --git a/tests/tmp_qf_jump/output_detail_o_test.go b/tests/tmp_qf_jump/output_detail_o_test.go new file mode 100644 index 0000000..cddd917 --- /dev/null +++ b/tests/tmp_qf_jump/output_detail_o_test.go @@ -0,0 +1,4 @@ +package foo + +func TestFoo(t *testing.T) { +}