diff --git a/README.md b/README.md index d5da035..4acc4a3 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ If no runner matches the current test file, test-samurai will show: Additional keymaps: - Listing navigation: - - `fn` -> [F]ind [N]ext failed test in listing (wraps to the first) - - `fp` -> [F]ind [P]revious failed test in listing (wraps to the last) - - `ff` -> [F]ind [F]irst list entry + - `fn` -> [F]ind [N]ext failed test in listing (wraps to the first, opens Detail-Float, works in Detail-Float) + - `fp` -> [F]ind [P]revious failed test in listing (wraps to the last, opens Detail-Float, works in Detail-Float) + - `ff` -> [F]ind [F]irst list entry (opens Detail-Float, works in Detail-Float) - `o` -> jump to the test location - `qn` -> close the testing floats and jump to the first quickfix entry - Listing filters: diff --git a/doc/test-samurai.txt b/doc/test-samurai.txt index 50cfd6d..b9847d4 100644 --- a/doc/test-samurai.txt +++ b/doc/test-samurai.txt @@ -52,8 +52,9 @@ In the Testing-Float, press ? to open the quick-help in the Detail-Float. Additional keymaps: Listing navigation: - fn [F]ind [N]ext failed test in listing - fp [F]ind [P]revious failed test in listing + fn [F]ind [N]ext failed test in listing (opens Detail-Float; works in Detail-Float) + fp [F]ind [P]revious failed test in listing (opens Detail-Float; works in Detail-Float) + ff [F]ind [F]irst list entry (opens Detail-Float; works in Detail-Float) o Jump to test location qn Close floats + jump to the first quickfix entry Listing filters: diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 59e0e25..7bbf738 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -65,9 +65,9 @@ local function help_lines() " TSamShowOutput to", "", "Listing navigation:", - " fn [F]ind [N]ext failed test in listing", - " fp [F]ind [P]revious failed test in listing", - " ff [F]ind [F]irst list entry", + " fn [F]ind [N]ext failed test in listing (opens Detail-Float; works in Detail-Float)", + " fp [F]ind [P]revious failed test in listing (opens Detail-Float; works in Detail-Float)", + " ff [F]ind [F]irst list entry (opens Detail-Float; works in Detail-Float)", " o Jump to test location", " qn Close floats + jump to first quickfix entry", "", @@ -278,11 +278,11 @@ 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 + return nil end local total = vim.api.nvim_buf_line_count(buf) if total == 0 then - return + return nil end local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) local cursor = vim.api.nvim_win_get_cursor(win) @@ -334,25 +334,52 @@ local function jump_listing_fail(direction) if target then vim.api.nvim_win_set_cursor(win, { target, 0 }) end + return target end local function jump_to_first_listing_entry() 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 + return nil end local total = vim.api.nvim_buf_line_count(buf) if total == 0 then - return + return nil end local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) for i = 1, total do if lines[i] and lines[i]:match("^%[ %u+ %] %- ") then vim.api.nvim_win_set_cursor(win, { i, 0 }) - return + return i end end + return nil +end + +local function jump_listing_and_open(kind) + local current_win = vim.api.nvim_get_current_win() + local listing_win = state.last_win + if listing_win and vim.api.nvim_win_is_valid(listing_win) then + vim.api.nvim_set_current_win(listing_win) + else + listing_win = current_win + end + local target = nil + if kind == "next" then + target = jump_listing_fail("next") + elseif kind == "prev" then + target = jump_listing_fail("prev") + elseif kind == "first" then + target = jump_to_first_listing_entry() + end + if target then + M.open_test_output_at_cursor() + return + end + if current_win and vim.api.nvim_win_is_valid(current_win) then + vim.api.nvim_set_current_win(current_win) + end end local function find_normal_window() @@ -859,13 +886,13 @@ local function create_output_win(initial_lines) M.toggle_detail_full() end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "fn", function() - jump_listing_fail("next") + jump_listing_and_open("next") end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "fp", function() - jump_listing_fail("prev") + jump_listing_and_open("prev") end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "ff", function() - jump_to_first_listing_entry() + jump_listing_and_open("first") end, { buffer = buf, nowait = true, silent = true, desc = "[F]ind [F]irst list entry" }) vim.keymap.set("n", "cb", function() M.listing_break_on_dashes() @@ -948,13 +975,13 @@ local function reopen_output_win() M.toggle_detail_full() end, { buffer = state.last_buf, nowait = true, silent = true }) vim.keymap.set("n", "fn", function() - jump_listing_fail("next") + jump_listing_and_open("next") end, { buffer = state.last_buf, nowait = true, silent = true }) vim.keymap.set("n", "fp", function() - jump_listing_fail("prev") + jump_listing_and_open("prev") end, { buffer = state.last_buf, nowait = true, silent = true }) vim.keymap.set("n", "ff", function() - jump_to_first_listing_entry() + jump_listing_and_open("first") end, { buffer = state.last_buf, nowait = true, silent = true, desc = "[F]ind [F]irst list entry" }) vim.keymap.set("n", "cb", function() M.listing_break_on_dashes() @@ -1304,6 +1331,15 @@ local function ensure_detail_buf(lines) vim.keymap.set("n", "z", function() M.toggle_detail_full() end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "fn", function() + jump_listing_and_open("next") + end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "fp", function() + jump_listing_and_open("prev") + end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "ff", function() + jump_listing_and_open("first") + end, { buffer = buf, nowait = true, silent = true, desc = "[F]ind [F]irst list entry" }) vim.keymap.set("n", "", function() close_detail_float() end, { buffer = buf, nowait = true, silent = true }) diff --git a/tests/test_samurai_core_spec.lua b/tests/test_samurai_core_spec.lua index 8f5fe30..23c0452 100644 --- a/tests/test_samurai_core_spec.lua +++ b/tests/test_samurai_core_spec.lua @@ -252,8 +252,8 @@ describe("test-samurai core (no bundled runners)", function() assert.is_true(joined:find("to", 1, true) ~= nil) assert.is_true(joined:find("fn", 1, true) ~= nil) assert.is_true(joined:find("fp", 1, true) ~= nil) - assert.is_true(joined:find("[F]ind [N]ext failed test in listing", 1, true) ~= nil) - assert.is_true(joined:find("[F]ind [P]revious failed test in listing", 1, true) ~= nil) + assert.is_true(joined:find("[F]ind [N]ext failed test in listing (opens Detail-Float; works in Detail-Float)", 1, true) ~= nil) + assert.is_true(joined:find("[F]ind [P]revious failed test in listing (opens Detail-Float; works in Detail-Float)", 1, true) ~= nil) assert.is_true(joined:find("cb", 1, true) ~= nil) assert.is_true(joined:find("cj", 1, true) ~= nil) assert.is_true(joined:find("breaks test-command onto multiple lines", 1, true) ~= nil) @@ -1010,7 +1010,7 @@ describe("test-samurai core (no bundled runners)", function() vim.fn.jobstart = orig_jobstart end) - it("maps ff to jump to the first listing entry", function() + it("maps ff to jump to the first listing entry and open the detail float", function() local runner = { name = "test-runner", } @@ -1092,14 +1092,8 @@ describe("test-samurai core (no bundled runners)", function() vim.fn.jobstart = orig_jobstart - local listing_buf = nil - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].filetype == "test-samurai-output" then - listing_buf = buf - break - end - end - assert.is_true(listing_buf ~= nil) + local listing_buf = vim.api.nvim_get_current_buf() + assert.is_true(vim.bo[listing_buf].filetype == "test-samurai-output") local maps = vim.api.nvim_buf_get_keymap(listing_buf, "n") local found = nil @@ -1113,6 +1107,7 @@ describe("test-samurai core (no bundled runners)", function() assert.equals("[F]ind [F]irst list entry", found.desc) vim.api.nvim_set_current_buf(listing_buf) + local listing_win = vim.api.nvim_get_current_win() local total = vim.api.nvim_buf_line_count(listing_buf) vim.api.nvim_win_set_cursor(0, { total, 0 }) assert.is_true(type(found.callback) == "function") @@ -1127,7 +1122,229 @@ describe("test-samurai core (no bundled runners)", function() end end assert.is_true(first_entry ~= nil) - local cursor = vim.api.nvim_win_get_cursor(0) + local cursor = vim.api.nvim_win_get_cursor(listing_win) assert.equals(first_entry, cursor[1]) + + local detail_buf = vim.api.nvim_get_current_buf() + local detail_lines = vim.api.nvim_buf_get_lines(detail_buf, 0, -1, false) + assert.same({ "", "No output captured" }, detail_lines) + end) + + it("opens the detail float after jumping to the next failed entry", function() + local runner = { + name = "test-runner-listing-fn", + } + + function runner.is_test_file(_bufnr) + return true + end + + function runner.find_nearest(bufnr, _row, _col) + return { file = vim.api.nvim_buf_get_name(bufnr), cwd = vim.loop.cwd(), test_name = "TestB" } + end + + function runner.build_command(spec) + return { cmd = { "echo", "nearest" }, cwd = spec.cwd } + end + + function runner.build_file_command(_bufnr) + return { cmd = { "echo", "file" } } + end + + function runner.build_all_command(_bufnr) + return { cmd = { "echo", "all" } } + end + + function runner.build_failed_command(last_command, _failures, _scope_kind) + return { cmd = { "echo", "failed" }, cwd = last_command and last_command.cwd or nil } + end + + function runner.parse_results(_output) + return { passes = {}, failures = { "TestB" }, skips = {} } + end + + function runner.output_parser() + return { + on_complete = function(output, _state) + return runner.parse_results(output) + end, + } + end + + function runner.parse_test_output(_output) + return {} + end + + function runner.collect_failed_locations(_failures, _command, _scope_kind) + return {} + end + + package.loaded["test-samurai-fn-runner"] = runner + test_samurai.setup({ runner_modules = { "test-samurai-fn-runner" } }) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/test_runner_listing_fn.go") + vim.bo[bufnr].filetype = "go" + vim.api.nvim_set_current_buf(bufnr) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts.on_exit then + opts.on_exit(nil, 0, nil) + end + return 1 + end + + core.run_nearest() + + vim.fn.jobstart = orig_jobstart + + local listing_buf = vim.api.nvim_get_current_buf() + assert.is_true(vim.bo[listing_buf].filetype == "test-samurai-output") + + local maps = vim.api.nvim_buf_get_keymap(listing_buf, "n") + local found = nil + for _, map in ipairs(maps) do + if type(map.lhs) == "string" and map.lhs:sub(-2) == "fn" then + found = map + break + end + end + assert.is_true(found ~= nil) + + vim.api.nvim_set_current_buf(listing_buf) + local listing_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + assert.is_true(type(found.callback) == "function") + found.callback() + + local lines = vim.api.nvim_buf_get_lines(listing_buf, 0, -1, false) + local fail_entry = nil + for i, line in ipairs(lines) do + if line:match("^%[ FAIL %] %- ") then + fail_entry = i + break + end + end + assert.is_true(fail_entry ~= nil) + local cursor = vim.api.nvim_win_get_cursor(listing_win) + assert.equals(fail_entry, cursor[1]) + + local detail_buf = vim.api.nvim_get_current_buf() + local detail_lines = vim.api.nvim_buf_get_lines(detail_buf, 0, -1, false) + assert.same({ "", "No output captured" }, detail_lines) + end) + + it("allows fn from the detail float to jump the listing and refresh detail", function() + local runner = { + name = "test-runner-detail-fn", + } + + function runner.is_test_file(_bufnr) + return true + end + + function runner.find_nearest(bufnr, _row, _col) + return { file = vim.api.nvim_buf_get_name(bufnr), cwd = vim.loop.cwd(), test_name = "TestC" } + end + + function runner.build_command(spec) + return { cmd = { "echo", "nearest" }, cwd = spec.cwd } + end + + function runner.build_file_command(_bufnr) + return { cmd = { "echo", "file" } } + end + + function runner.build_all_command(_bufnr) + return { cmd = { "echo", "all" } } + end + + function runner.build_failed_command(last_command, _failures, _scope_kind) + return { cmd = { "echo", "failed" }, cwd = last_command and last_command.cwd or nil } + end + + function runner.parse_results(_output) + return { passes = {}, failures = { "TestC" }, skips = {} } + end + + function runner.output_parser() + return { + on_complete = function(output, _state) + return runner.parse_results(output) + end, + } + end + + function runner.parse_test_output(_output) + return {} + end + + function runner.collect_failed_locations(_failures, _command, _scope_kind) + return {} + end + + package.loaded["test-samurai-detail-fn-runner"] = runner + test_samurai.setup({ runner_modules = { "test-samurai-detail-fn-runner" } }) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/test_runner_detail_fn.go") + vim.bo[bufnr].filetype = "go" + vim.api.nvim_set_current_buf(bufnr) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts.on_exit then + opts.on_exit(nil, 0, nil) + end + return 1 + end + + core.run_nearest() + + vim.fn.jobstart = orig_jobstart + + local listing_buf = vim.api.nvim_get_current_buf() + assert.is_true(vim.bo[listing_buf].filetype == "test-samurai-output") + + local lines = vim.api.nvim_buf_get_lines(listing_buf, 0, -1, false) + local fail_entry = nil + for i, line in ipairs(lines) do + if line:match("^%[ FAIL %] %- ") then + fail_entry = i + break + end + end + assert.is_true(fail_entry ~= nil) + + local listing_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(listing_win, { fail_entry, 0 }) + core.open_test_output_at_cursor() + + local detail_win = vim.api.nvim_get_current_win() + local detail_buf = vim.api.nvim_get_current_buf() + local maps = vim.api.nvim_buf_get_keymap(detail_buf, "n") + local found = nil + for _, map in ipairs(maps) do + if type(map.lhs) == "string" and map.lhs:sub(-2) == "fn" then + found = map + break + end + end + assert.is_true(found ~= nil) + + vim.api.nvim_set_current_win(listing_win) + vim.api.nvim_win_set_cursor(listing_win, { 1, 0 }) + vim.api.nvim_set_current_win(detail_win) + + assert.is_true(type(found.callback) == "function") + found.callback() + + local cursor = vim.api.nvim_win_get_cursor(listing_win) + assert.equals(fail_entry, cursor[1]) + + local refreshed_buf = vim.api.nvim_get_current_buf() + local detail_lines = vim.api.nvim_buf_get_lines(refreshed_buf, 0, -1, false) + assert.same({ "", "No output captured" }, detail_lines) end) end)