From e92c8476c225c5858a4c42260232e0717d37675c Mon Sep 17 00:00:00 2001 From: "M.Schirmer" Date: Tue, 23 Dec 2025 22:10:47 +0100 Subject: [PATCH] finish first MVP with test-runner-detection for Go, Mocha.js, Jest.js and ViTest.js --- .gitignore | 1 + lua/test-samurai/core.lua | 240 ++++++++++++++++++++++++------- lua/test-samurai/runners/go.lua | 40 +++++- lua/test-samurai/runners/js.lua | 192 +++++++++++++++++++++++-- run_test.sh | 3 + tests/test_samurai_core_spec.lua | 47 ++++++ tests/test_samurai_go_spec.lua | 42 +++++- tests/test_samurai_js_spec.lua | 117 +++++++++++++-- 8 files changed, 593 insertions(+), 89 deletions(-) create mode 100644 .gitignore create mode 100644 run_test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53752db --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +output diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 641cf57..ada304c 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -1,4 +1,5 @@ local config = require("test-samurai.config") +local util = require("test-samurai.util") local M = {} @@ -30,19 +31,105 @@ function M.reload_runners() load_runners() end +local function detect_js_framework(file) + local root = util.find_root(file, { "package.json", "node_modules" }) + if not root or root == "" then + return nil + end + + local pkg_path = vim.fs.joinpath(root, "package.json") + local stat = vim.loop.fs_stat(pkg_path) + if not stat or stat.type ~= "file" then + return nil + end + + local ok_read, lines = pcall(vim.fn.readfile, pkg_path) + if not ok_read or type(lines) ~= "table" then + return nil + end + + local json = table.concat(lines, "\n") + local ok_json, pkg = pcall(vim.json.decode, json) + if not ok_json or type(pkg) ~= "table" then + return nil + end + + local present = {} + + local function scan_section(section) + if type(section) ~= "table" then + return + end + for name, _ in pairs(section) do + if name == "mocha" or name == "jest" or name == "vitest" then + present[name] = true + end + end + end + + scan_section(pkg.dependencies) + scan_section(pkg.devDependencies) + + if next(present) == nil then + return nil + end + + return present +end + function M.get_runner_for_buf(bufnr) + local path = util.get_buf_path(bufnr) + + local candidates = {} + for _, runner in ipairs(state.runners) do if type(runner.is_test_file) == "function" then local ok, is_test = pcall(runner.is_test_file, bufnr) if ok and is_test then - return runner + table.insert(candidates, runner) end end end + + if #candidates == 1 then + return candidates[1] + elseif #candidates > 1 then + local frameworks = nil + if path and path ~= "" then + frameworks = detect_js_framework(path) + end + if frameworks then + for _, runner in ipairs(candidates) do + if runner.framework and frameworks[runner.framework] then + return runner + end + end + end + return candidates[1] + end + + if not path or path == "" then + return nil + end + + if path:sub(-8) == "_test.go" then + local ok, go = pcall(require, "test-samurai.runners.go") + if ok and type(go) == "table" then + return go + end + end + + if path:find(".test.", 1, true) or path:find(".spec.", 1, true) then + local ok, jsjest = pcall(require, "test-samurai.runners.js-jest") + if ok and type(jsjest) == "table" then + return jsjest + end + end + return nil end -local function open_float(lines) +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) end @@ -58,7 +145,7 @@ local function open_float(lines) local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe") vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output") - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_lines or {}) local win = vim.api.nvim_open_win(buf, true, { relative = "editor", @@ -78,42 +165,59 @@ local function open_float(lines) state.last_win = win state.last_buf = buf + + return buf, win end -local function run_cmd(cmd, cwd, on_exit) - if vim.system then - vim.system(cmd, { cwd = cwd, text = true }, function(obj) - local code = obj.code or -1 - local stdout = obj.stdout or "" - local stderr = obj.stderr or "" - vim.schedule(function() - on_exit(code, stdout, stderr) - end) - end) - else - local stdout_chunks = {} - local stderr_chunks = {} - vim.fn.jobstart(cmd, { - cwd = cwd, - stdout_buffered = true, - stderr_buffered = true, - on_stdout = function(_, data) - if data then - table.insert(stdout_chunks, table.concat(data, "\n")) - end - end, - on_stderr = function(_, data) - if data then - table.insert(stderr_chunks, table.concat(data, "\n")) - end - end, - on_exit = function(_, code) - local stdout = table.concat(stdout_chunks, "\n") - local stderr = table.concat(stderr_chunks, "\n") - on_exit(code, stdout, stderr) - end, - }) +local function append_lines(buf, new_lines) + if not (buf and vim.api.nvim_buf_is_valid(buf)) then + return end + if not new_lines or #new_lines == 0 then + return + end + local existing = vim.api.nvim_buf_line_count(buf) + vim.api.nvim_buf_set_lines(buf, existing, existing, false, new_lines) +end + +local function run_cmd(cmd, cwd, handlers) + local h = handlers or {} + + if h.on_start then + pcall(h.on_start) + end + + local function handle_chunk(fn, data) + if not fn or not data then + return + end + local lines = {} + for _, line in ipairs(data) do + if line ~= nil and line ~= "" then + table.insert(lines, line) + end + end + if #lines > 0 then + fn(lines) + end + end + + vim.fn.jobstart(cmd, { + cwd = cwd, + stdout_buffered = false, + stderr_buffered = false, + on_stdout = function(_, data, _) + handle_chunk(h.on_stdout, data) + end, + on_stderr = function(_, data, _) + handle_chunk(h.on_stderr, data) + end, + on_exit = function(_, code, _) + if h.on_exit then + pcall(h.on_exit, code or 0) + end + end, + }) end function M.run_nearest() @@ -153,23 +257,53 @@ function M.run_nearest() local cmd = command.cmd local cwd = command.cwd or vim.loop.cwd() - run_cmd(cmd, cwd, function(code, stdout, stderr) - local header = "$ " .. table.concat(cmd, " ") - local lines = { header, "" } - if stdout ~= "" then - local out_lines = vim.split(stdout, "\n", { plain = true }) - vim.list_extend(lines, out_lines) - end - if stderr ~= "" then - table.insert(lines, "") - table.insert(lines, "[stderr]") - local err_lines = vim.split(stderr, "\n", { plain = true }) - vim.list_extend(lines, err_lines) - end - table.insert(lines, "") - table.insert(lines, "[exit code] " .. tostring(code)) - open_float(lines) - end) + local header = "$ " .. table.concat(cmd, " ") + local buf = nil + local has_output = false + + run_cmd(cmd, cwd, { + on_start = function() + buf = select(1, create_output_win({ header, "", "[running...]" })) + end, + on_stdout = function(lines) + if not buf then + buf = select(1, create_output_win({ header, "" })) + end + if not has_output then + local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + if #cur > 0 and cur[#cur] == "[running...]" then + vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {}) + end + has_output = true + end + append_lines(buf, lines) + end, + on_stderr = function(lines) + if not buf then + buf = select(1, create_output_win({ header, "" })) + end + if not has_output then + local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + if #cur > 0 and cur[#cur] == "[running...]" then + vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {}) + end + has_output = true + end + append_lines(buf, lines) + end, + on_exit = function(code) + if not buf then + buf = select(1, create_output_win({ header })) + end + if not has_output then + local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + if #cur > 0 and cur[#cur] == "[running...]" then + vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {}) + end + end + append_lines(buf, { "", "[exit code] " .. tostring(code) }) + end, + }) end return M diff --git a/lua/test-samurai/runners/go.lua b/lua/test-samurai/runners/go.lua index d714f0e..a0d3550 100644 --- a/lua/test-samurai/runners/go.lua +++ b/lua/test-samurai/runners/go.lua @@ -67,9 +67,38 @@ local function find_t_runs(lines, func) return subtests end -local function escape_pattern(str) - local escaped = str:gsub("(%W)", "%%%1") - return "^" .. escaped .. "$" +local function build_run_pattern(spec) + local name = spec.test_path or "" + local escaped = name:gsub("(%W)", "%%%1") + if spec.scope == "function" then + return "^" .. escaped .. "$|^" .. escaped .. "/" + else + return "^" .. escaped .. "$" + end +end + +local function build_pkg_arg(spec) + local file = spec.file + local cwd = spec.cwd + if not file or not cwd or file == "" or cwd == "" then + return "./..." + end + + local dir = vim.fs.dirname(file) + if dir == cwd then + return "./" + end + + if file:sub(1, #cwd) ~= cwd then + return "./..." + end + + local rel = dir:sub(#cwd + 2) + if not rel or rel == "" then + return "./" + end + + return "./" .. rel end function runner.is_test_file(bufnr) @@ -134,8 +163,9 @@ function runner.find_nearest(bufnr, row, _col) end function runner.build_command(spec) - local pattern = escape_pattern(spec.test_path) - local cmd = { "go", "test", "./...", "-run", pattern } + local pattern = build_run_pattern(spec) + local pkg = build_pkg_arg(spec) + local cmd = { "go", "test", "-v", pkg, "-run", pattern } return { cmd = cmd, cwd = spec.cwd, diff --git a/lua/test-samurai/runners/js.lua b/lua/test-samurai/runners/js.lua index ad3f0fe..d8de703 100644 --- a/lua/test-samurai/runners/js.lua +++ b/lua/test-samurai/runners/js.lua @@ -28,8 +28,168 @@ local function is_js_test_file(bufnr, filetypes, patterns) return false end +local function find_block_end(lines, start_idx) + local depth = 0 + local started = false + for i = start_idx, #lines do + local line = lines[i] + for j = 1, #line do + local ch = line:sub(j, j) + if ch == "{" then + depth = depth + 1 + started = true + elseif ch == "}" then + if started then + depth = depth - 1 + if depth == 0 then + return i - 1 + end + end + end + end + end + return #lines - 1 +end + +local jest_config_files = { "jest.config.js", "jest.config.ts" } + +local function find_jest_root(file) + if not file or file == "" then + return util.find_root(file, { + "jest.config.js", + "jest.config.ts", + "vitest.config.ts", + "vitest.config.js", + "package.json", + "node_modules", + }) + end + + local dir = vim.fs.dirname(file) + local found = vim.fs.find(jest_config_files, { path = dir, upward = true }) + if found and #found > 0 then + local cfg = found[1] + local sep = package.config:sub(1, 1) + local marker = sep .. "test" .. sep .. ".bin" .. sep + local idx = cfg:find(marker, 1, true) + if idx then + local root = cfg:sub(1, idx - 1) + if root == "" then + root = sep + end + return root + end + return vim.fs.dirname(cfg) + end + + return util.find_root(file, { + "jest.config.js", + "jest.config.ts", + "vitest.config.ts", + "vitest.config.js", + "package.json", + "node_modules", + }) +end + +local function match_test_call(line) + local call, name = line:match("^%s*(it)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if call then + return call, name + end + call, name = line:match("^%s*(test)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if call then + return call, name + end + call, name = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if call and name then + return call, name + end + return nil, nil +end + +local function collect_js_structs(lines) + local describes = {} + local tests = {} + + for i, line in ipairs(lines) do + local dcall, dname = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if dcall and dname then + local start0 = i - 1 + local end0 = find_block_end(lines, i) + table.insert(describes, { + kind = dcall, + name = dname, + start = start0, + ["end"] = end0, + }) + else + local tcall, tname = line:match("^%s*(it)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if not tcall then + tcall, tname = line:match("^%s*(test)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + end + if tcall and tname then + local start0 = i - 1 + local end0 = find_block_end(lines, i) + table.insert(tests, { + kind = tcall, + name = tname, + start = start0, + ["end"] = end0, + }) + end + end + end + + return describes, tests +end + +local function build_full_name(lines, idx, leaf_name) + local parts = { leaf_name } + for i = idx - 1, 1, -1 do + local line = lines[i] + local call, name = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if call and name then + table.insert(parts, 1, name) + end + end + return table.concat(parts, " ") +end + local function find_nearest_test(bufnr, row) local lines = util.get_buf_lines(bufnr) + local describes, tests = collect_js_structs(lines) + + for _, t in ipairs(tests) do + if row >= t.start and row <= t["end"] then + local full = build_full_name(lines, t.start + 1, t.name) + return { + kind = t.kind, + name = t.name, + full_name = full, + line = t.start, + } + end + end + + local best_describe = nil + for _, d in ipairs(describes) do + if row >= d.start and row <= d["end"] then + if not best_describe or d.start >= best_describe.start then + best_describe = d + end + end + end + if best_describe then + local full = build_full_name(lines, best_describe.start + 1, best_describe.name) + return { + kind = "describe", + name = best_describe.name, + full_name = full, + line = best_describe.start, + } + end + local start = row + 1 if start > #lines then start = #lines @@ -38,15 +198,18 @@ local function find_nearest_test(bufnr, row) end for i = start, 1, -1 do local line = lines[i] - local call, name = line:match("^%s*(it|test|describe)%s*%(%s*['"`]([^'"`]+)['"`]") + local call, name = match_test_call(line) if call and name then + local full = build_full_name(lines, i, name) return { kind = call, name = name, + full_name = full, line = i - 1, } end end + return nil end @@ -82,19 +245,25 @@ function M.new(opts) return nil, "no test call found" end local path = util.get_buf_path(bufnr) - local root = util.find_root(path, { - "jest.config.js", - "jest.config.ts", - "vitest.config.ts", - "vitest.config.js", - "package.json", - "node_modules", - }) + local root + if runner.framework == "jest" then + root = find_jest_root(path) + else + root = util.find_root(path, { + "jest.config.js", + "jest.config.ts", + "vitest.config.ts", + "vitest.config.js", + "package.json", + "node_modules", + }) + end return { file = path, cwd = root, framework = runner.framework, test_name = hit.name, + full_name = hit.full_name, kind = hit.kind, } end @@ -109,9 +278,10 @@ function M.new(opts) local function build_mocha(spec) local cmd = vim.deepcopy(runner.command) + local target = spec.full_name or spec.test_name + table.insert(cmd, "--fgrep") + table.insert(cmd, target) table.insert(cmd, spec.file) - table.insert(cmd, "--grep") - table.insert(cmd, spec.test_name) return cmd end diff --git a/run_test.sh b/run_test.sh new file mode 100644 index 0000000..b68cb9d --- /dev/null +++ b/run_test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests" -c qa diff --git a/tests/test_samurai_core_spec.lua b/tests/test_samurai_core_spec.lua index 722f8ae..8004dff 100644 --- a/tests/test_samurai_core_spec.lua +++ b/tests/test_samurai_core_spec.lua @@ -1,9 +1,17 @@ local test_samurai = require("test-samurai") local core = require("test-samurai.core") +local util = require("test-samurai.util") + +local orig_find_root = util.find_root +local orig_fs_stat = vim.loop.fs_stat +local orig_readfile = vim.fn.readfile describe("test-samurai core", function() before_each(function() test_samurai.setup() + util.find_root = orig_find_root + vim.loop.fs_stat = orig_fs_stat + vim.fn.readfile = orig_readfile end) it("selects Go runner for _test.go files", function() @@ -23,4 +31,43 @@ describe("test-samurai core", function() assert.is_not_nil(runner) assert.equals("js-jest", runner.name) end) + + it("prefers mocha runner when mocha is in package.json", function() + util.find_root = function(_, _) + return "/tmp/mocha_proj" + end + + vim.loop.fs_stat = function(path) + if path == "/tmp/mocha_proj/package.json" then + return { type = "file" } + end + return nil + end + + vim.fn.readfile = function(path) + if path == "/tmp/mocha_proj/package.json" then + return { + "{", + ' "devDependencies": { "mocha": "^10.0.0" }', + "}", + } + end + return {} + end + + test_samurai.setup() + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/mocha_proj/foo.test.ts") + vim.bo[bufnr].filetype = "typescript" + + local runner = core.get_runner_for_buf(bufnr) + + util.find_root = orig_find_root + vim.loop.fs_stat = orig_fs_stat + vim.fn.readfile = orig_readfile + + assert.is_not_nil(runner) + assert.equals("js-mocha", runner.name) + end) end) diff --git a/tests/test_samurai_go_spec.lua b/tests/test_samurai_go_spec.lua index bc6498a..ae98b4e 100644 --- a/tests/test_samurai_go_spec.lua +++ b/tests/test_samurai_go_spec.lua @@ -3,17 +3,17 @@ local go_runner = require("test-samurai.runners.go") describe("test-samurai go runner", function() it("detects Go test files by suffix", function() local bufnr1 = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo_test.go") + vim.api.nvim_buf_set_name(bufnr1, "/tmp/go_suffix_test.go") assert.is_true(go_runner.is_test_file(bufnr1)) local bufnr2 = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo.go") + vim.api.nvim_buf_set_name(bufnr2, "/tmp/go_main.go") assert.is_false(go_runner.is_test_file(bufnr2)) end) it("finds subtest when cursor is inside t.Run block", function() local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_test.go") + vim.api.nvim_buf_set_name(bufnr, "/tmp/go_subtest_test.go") local lines = { "package main", "import \"testing\"", @@ -44,13 +44,13 @@ describe("test-samurai go runner", function() assert.is_not_nil(spec) assert.equals("TestFoo/first", spec.test_path) assert.equals("subtest", spec.scope) - assert.equals("/tmp/foo_test.go", spec.file) - assert.equals("/tmp", spec.cwd) + assert.is_true(spec.file:match("go_subtest_test%.go$") ~= nil) + assert.is_true(spec.cwd:match("tmp$") ~= nil) end) it("falls back to whole test function when between subtests", function() local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_test.go") + vim.api.nvim_buf_set_name(bufnr, "/tmp/go_between_test.go") local lines = { "package main", "import \"testing\"", @@ -81,5 +81,35 @@ describe("test-samurai go runner", function() assert.is_not_nil(spec) assert.equals("TestFoo", spec.test_path) assert.equals("function", spec.scope) + assert.is_true(spec.file:match("go_between_test%.go$") ~= nil) + assert.is_true(spec.cwd:match("tmp$") ~= nil) + end) + + it("build_command uses current package and correct run pattern", function() + local spec_sub = { + file = "/tmp/project/pkg/foo_test.go", + cwd = "/tmp/project", + test_path = "TestFoo/first", + scope = "subtest", + } + + local cmd_spec_sub = go_runner.build_command(spec_sub) + assert.are.same( + { "go", "test", "-v", "./pkg", "-run", "^TestFoo%/first$" }, + cmd_spec_sub.cmd + ) + + local spec_func = { + file = "/tmp/project/foo_test.go", + cwd = "/tmp/project", + test_path = "TestFoo", + scope = "function", + } + + local cmd_spec_func = go_runner.build_command(spec_func) + assert.are.same( + { "go", "test", "-v", "./", "-run", "^TestFoo$|^TestFoo/" }, + cmd_spec_func.cmd + ) end) end) diff --git a/tests/test_samurai_js_spec.lua b/tests/test_samurai_js_spec.lua index 9df34ac..74e5808 100644 --- a/tests/test_samurai_js_spec.lua +++ b/tests/test_samurai_js_spec.lua @@ -1,29 +1,30 @@ local jest = require("test-samurai.runners.js-jest") +local mocha = require("test-samurai.runners.js-mocha") describe("test-samurai js runner (jest)", function() it("detects JS/TS test files by name and filetype", function() local bufnr1 = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo.test.ts") + vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo_detect.test.ts") vim.bo[bufnr1].filetype = "typescript" assert.is_true(jest.is_test_file(bufnr1)) local bufnr2 = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo.ts") + vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo_detect.ts") vim.bo[bufnr2].filetype = "typescript" assert.is_false(jest.is_test_file(bufnr2)) end) - it("finds nearest it() call as test name", function() + it("finds nearest it() call as test name and builds full_name when cursor is inside the test", function() local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, "/tmp/foo.test.ts") + vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_nearest.test.ts") vim.bo[bufnr].filetype = "typescript" local lines = { - "describe(\"outer\", function() {", - " it(\"inner 1\", function() {", + 'describe("outer", function() {', + ' it("inner 1", function() {', " -- inside 1", " })", "", - " it(\"inner 2\", function() {", + ' it("inner 2", function() {', " -- inside 2", " })", "})", @@ -31,10 +32,85 @@ describe("test-samurai js runner (jest)", function() vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) local orig_fs_find = vim.fs.find - vim.fs.find = function(markers, opts) + vim.fs.find = function(names, opts) return { "/tmp/package.json" } end + -- Cursor in der zweiten it()-Body + local row_inside_second = 6 -- 0-basiert -> Zeile mit "-- inside 2" + local spec, err = jest.find_nearest(bufnr, row_inside_second, 0) + + vim.fs.find = orig_fs_find + + assert.is_nil(err) + assert.is_not_nil(spec) + assert.equals("inner 2", spec.test_name) + assert.equals("outer inner 2", spec.full_name) + assert.equals("jest", spec.framework) + assert.is_true(spec.file:match("foo_nearest%.test%.ts$") ~= nil) + assert.is_true(spec.cwd:match("tmp$") ~= nil) + + local cmd_spec = jest.build_command(spec) + assert.are.same({ "npx", "jest", spec.file, "-t", "inner 2" }, cmd_spec.cmd) + end) + + it("returns describe block when cursor is between it() calls", function() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_between.test.ts") + vim.bo[bufnr].filetype = "typescript" + local lines = { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + } + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + local orig_fs_find = vim.fs.find + vim.fs.find = function(names, opts) + return { "/tmp/package.json" } + end + + -- Cursor auf der Leerzeile zwischen den beiden it()-Blöcken + local row_between = 4 -- 0-basiert -> leere Zeile zwischen den Tests + local spec, err = jest.find_nearest(bufnr, row_between, 0) + + vim.fs.find = orig_fs_find + + assert.is_nil(err) + assert.is_not_nil(spec) + assert.equals("outer", spec.test_name) + assert.equals("outer", spec.full_name) + assert.equals("jest", spec.framework) + end) + + it("treats jest.config in test/.bin as project root parent", function() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_binroot.test.ts") + vim.bo[bufnr].filetype = "typescript" + local lines = { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "", + ' it("inner 2", function() {', + " -- inside 2", + " })", + "})", + } + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + local orig_fs_find = vim.fs.find + vim.fs.find = function(names, opts) + return { "/tmp/test/.bin/jest.config.js" } + end + local row_inside_second = 6 local spec, err = jest.find_nearest(bufnr, row_inside_second, 0) @@ -42,12 +118,25 @@ describe("test-samurai js runner (jest)", function() assert.is_nil(err) assert.is_not_nil(spec) - assert.equals("inner 2", spec.test_name) - assert.equals("jest", spec.framework) - assert.equals("/tmp/foo.test.ts", spec.file) assert.equals("/tmp", spec.cwd) - - local cmd_spec = jest.build_command(spec) - assert.are.same({ "npx", "jest", "/tmp/foo.test.ts", "-t", "inner 2" }, cmd_spec.cmd) + end) +end) + +describe("test-samurai js runner (mocha)", function() + it("builds mocha command with fgrep and full test title", function() + local spec = { + file = "/tmp/project/test/foo_nearest.test.ts", + cwd = "/tmp/project", + test_name = "inner 2", + full_name = "outer inner 2", + } + + local cmd_spec = mocha.build_command(spec) + + assert.are.same( + { "npx", "mocha", "--fgrep", "outer inner 2", spec.file }, + cmd_spec.cmd + ) + assert.equals("/tmp/project", cmd_spec.cwd) end) end)