diff --git a/README.md b/README.md index 7cd7bd3..f1738bc 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Additional keymaps: - `sf` -> filter the listing to `[ FAIL ] - ...` entries - `ss` -> filter the listing to `[ SKIP ] - ...` entries - `sa` -> clear the listing filter and show all entries +- `tt` -> run the test under the cursor in the listing - `?` -> show help with TSam commands and standard keymaps in the Detail-Float ## Output UI diff --git a/doc/test-samurai.txt b/doc/test-samurai.txt index 2536867..e39193e 100644 --- a/doc/test-samurai.txt +++ b/doc/test-samurai.txt @@ -57,6 +57,7 @@ Additional keymaps: sf Filter listing to [ FAIL ] only ss Filter listing to [ SKIP ] only sa Show all listing entries (clear filter) + tt Run the test under the cursor in the listing o Jump to test location z Toggle Detail-Float full width Focus Detail-Float (press l again for full) diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 6465dce..b79a97d 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -40,6 +40,7 @@ local close_detail_float local jump_to_first_quickfix local apply_summary_highlights local apply_result_highlights +local run_command local function disable_container_maps(buf) local opts = { buffer = buf, nowait = true, silent = true } @@ -66,6 +67,7 @@ local function help_lines() " sf Filter listing to [ FAIL ] only", " ss Filter listing to [ SKIP ] only", " sa Show all listing entries (clear filter)", + " tt Run the test under the cursor", "", "Testing-Float (Listing):", " Open Detail-Float for selected test", @@ -779,6 +781,9 @@ local function create_output_win(initial_lines) vim.keymap.set("n", "sa", function() M.filter_listing_all() end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "tt", function() + M.run_test_at_cursor() + end, { buffer = buf, nowait = true, silent = true }) vim.keymap.set("n", "?", function() M.show_help() end, { buffer = buf, nowait = true, silent = true }) @@ -855,6 +860,9 @@ local function reopen_output_win() vim.keymap.set("n", "sa", function() M.filter_listing_all() end, { buffer = state.last_buf, nowait = true, silent = true }) + vim.keymap.set("n", "tt", function() + M.run_test_at_cursor() + end, { buffer = state.last_buf, nowait = true, silent = true }) vim.keymap.set("n", "?", function() M.show_help() end, { buffer = state.last_buf, nowait = true, silent = true }) @@ -1324,6 +1332,70 @@ function M.filter_listing_all() apply_listing_filter("all") end +function M.run_test_at_cursor() + 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 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.build_command) ~= "function" then + vim.notify("[test-samurai] Runner missing methods", vim.log.levels.ERROR) + return + end + + local command_src = state.last_scope_command or state.last_command + if not command_src then + vim.notify("[test-samurai] No previous test command", vim.log.levels.WARN) + return + end + + local spec = { + file = command_src.file, + cwd = command_src.cwd, + test_name = test_name, + full_name = test_name, + } + if runner._last_mocha_titles and type(runner._last_mocha_titles) == "table" then + spec.mocha_full_title = runner._last_mocha_titles[test_name] + end + if not spec.mocha_full_title and test_name:find("/", 1, true) then + spec.mocha_full_title = test_name:gsub("/", " ") + end + if not spec.file or spec.file == "" then + vim.notify("[test-samurai] Missing test file for rerun", vim.log.levels.WARN) + return + end + + local ok_cmd, command = pcall(runner.build_command, spec) + if not ok_cmd or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then + vim.notify("[test-samurai] Runner failed to build command", vim.log.levels.ERROR) + return + end + command.file = spec.file + + local parser = runner.output_parser + if type(parser) == "function" then + parser = parser() + end + run_command(command, { + track_scope = true, + runner = runner, + scope_kind = "nearest", + output_parser = parser or runner.parse_results, + }) +end + function M.focus_listing() if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then @@ -1776,7 +1848,7 @@ local function collect_failure_names_from_listing() return out end -local function run_command(command, opts) +run_command = function(command, opts) local options = opts or {} state.last_test_outputs = {} state.last_result_line_map = {} diff --git a/tests/test_samurai_core_spec.lua b/tests/test_samurai_core_spec.lua index ff91527..1509698 100644 --- a/tests/test_samurai_core_spec.lua +++ b/tests/test_samurai_core_spec.lua @@ -418,4 +418,102 @@ describe("test-samurai core (no bundled runners)", function() vim.fn.jobstart = orig_jobstart end) + + it("runs the test under the cursor from the listing", function() + local runner = { + name = "test-runner-run-cursor", + } + + 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 = "TestA" } + end + + local build_specs = {} + function runner.build_command(spec) + table.insert(build_specs, vim.deepcopy(spec)) + return { cmd = { "echo", "single" }, 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 = { "TestA" }, failures = { "TestC" }, skips = { "TestB" } } + end + + function runner.output_parser() + return { + on_line = function(_line, _state) + return nil + end, + 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-run-cursor-runner"] = runner + test_samurai.setup({ runner_modules = { "test-samurai-run-cursor-runner" } }) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/test_samurai_run_cursor.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 + + runner._last_mocha_titles = { TestC = "Suite TestC" } + core.run_nearest() + + local listing_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(listing_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_true(target ~= nil) + + vim.api.nvim_win_set_cursor(0, { target, 0 }) + core.run_test_at_cursor() + + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + core.run_test_at_cursor() + + vim.fn.jobstart = orig_jobstart + + assert.equals(2, #build_specs) + assert.equals("TestC", build_specs[2].test_name) + assert.equals("TestC", build_specs[2].full_name) + assert.equals("Suite TestC", build_specs[2].mocha_full_title) + end) end)