From ebfcaef2403c3965d1a55c1125a03cfc9bde98b4 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Fri, 8 May 2026 03:12:41 +0200 Subject: [PATCH] fix(git/status_view): scope dispatch to current tabpage + test cleanup --- .emmyrc.json | 5 +- lua/git/status_view.lua | 29 +++++---- test/git/cmd_test.lua | 116 ++++++++++++++++++---------------- test/git/helpers.lua | 11 +++- test/git/repo_test.lua | 66 ++++++++++--------- test/git/status_view_test.lua | 107 ++++++++++++++++++------------- test/init.lua | 21 +++++- test/runner.lua | 16 +++-- 8 files changed, 217 insertions(+), 154 deletions(-) diff --git a/.emmyrc.json b/.emmyrc.json index ad70c06..4a7ba3e 100644 --- a/.emmyrc.json +++ b/.emmyrc.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json", "runtime": { "version": "LuaJIT", - "requirePattern": ["lua/?.lua", "lua/?/init.lua"] + "requirePattern": ["?.lua", "?/init.lua", "lua/?.lua", "lua/?/init.lua"] }, "diagnostics": { "disable": ["unnecessary-if", "preferred-local-alias", "redefined-local"] @@ -23,8 +23,7 @@ "~/.local/share/nvim/site/pack/core/opt/nvim-tree.lua", "~/.local/share/nvim/site/pack/core/opt/oil.nvim", "~/.local/share/nvim/site/pack/core/opt/outline.nvim", - "~/.local/share/nvim/site/pack/core/opt/blink.cmp", - "./test" + "~/.local/share/nvim/site/pack/core/opt/blink.cmp" ] } } diff --git a/lua/git/status_view.lua b/lua/git/status_view.lua index a1ad66c..0bec220 100644 --- a/lua/git/status_view.lua +++ b/lua/git/status_view.lua @@ -62,14 +62,23 @@ local function find_view() end end +---@param win integer? +---@return boolean +local function valid_in_current_tab(win) + if not win or not vim.api.nvim_win_is_valid(win) then + return false + end + return vim.api.nvim_win_get_tabpage(win) + == vim.api.nvim_get_current_tabpage() +end + ---@param s ow.Git.StatusView.State ---@return integer? local function win_for(s) - local win = s.win - if win and vim.api.nvim_win_is_valid(win) then - return win + if valid_in_current_tab(s.win) then + return s.win end - win = find_view() + local win = find_view() s.win = win return win end @@ -267,14 +276,10 @@ end ---@return integer? left ---@return integer? right local function adopt_diff_wins(s, status_win) - local left = s.diff_left_win - local right = s.diff_right_win - if left and not vim.api.nvim_win_is_valid(left) then - left = nil - end - if right and not vim.api.nvim_win_is_valid(right) then - right = nil - end + local left = valid_in_current_tab(s.diff_left_win) and s.diff_left_win + or nil + local right = valid_in_current_tab(s.diff_right_win) and s.diff_right_win + or nil if left and right then return left, right end diff --git a/test/git/cmd_test.lua b/test/git/cmd_test.lua index 6ba531d..95f134d 100644 --- a/test/git/cmd_test.lua +++ b/test/git/cmd_test.lua @@ -1,15 +1,13 @@ -local t = require("test") -local helpers = require("test.git.helpers") local cmd = require("git.cmd") +local h = require("test.git.helpers") +local t = require("test") require("git").init() -local git = helpers.git - ---@param files table? ---@return string dir local function make_repo(files) - return helpers.make_repo(files, { cd = true }) + return h.make_repo(files, { cd = true }) end ---@param actual string[] @@ -49,12 +47,9 @@ t.test("parse_args handles escaped quote inside double quotes", function() t.eq(cmd.parse_args([["a\"b" c]]), { 'a"b', "c" }) end) -t.test( - "parse_args treats backslash literally inside single quotes", - function() - t.eq(cmd.parse_args([['a\b' c]]), { "a\\b", "c" }) - end -) +t.test("parse_args treats backslash literally inside single quotes", function() + t.eq(cmd.parse_args([['a\b' c]]), { "a\\b", "c" }) +end) t.test("parse_args concatenates adjacent quoted segments", function() t.eq(cmd.parse_args([[foo"bar"baz]]), { "foobarbaz" }) @@ -162,19 +157,19 @@ end) t.test("complete branch returns plain refs (no pseudo, no stash)", function() local dir = make_repo({ a = "x" }) - git(dir, "branch", "feature") - git(dir, "tag", "v1") + h.git(dir, "branch", "feature") + h.git(dir, "tag", "v1") t.write(dir, "a", "modified") - git(dir, "stash") + h.git(dir, "stash") local matches = cmd.complete("", "G branch ", 9) eq_sorted(matches, { "feature", "main", "v1" }) end) t.test("complete merge returns refs + pseudo + stash", function() local dir = make_repo({ a = "x" }) - git(dir, "branch", "feature") + h.git(dir, "branch", "feature") t.write(dir, "a", "y") - git(dir, "stash") + h.git(dir, "stash") local matches = cmd.complete("", "G merge ", 8) eq_sorted( matches, @@ -184,15 +179,15 @@ end) t.test("complete push first positional returns remotes", function() local dir = make_repo({ a = "x" }) - git(dir, "remote", "add", "origin", "/tmp/nope") - git(dir, "remote", "add", "upstream", "/tmp/nope") + h.git(dir, "remote", "add", "origin", "/tmp/nope") + h.git(dir, "remote", "add", "upstream", "/tmp/nope") local matches = cmd.complete("", "G push ", 7) eq_sorted(matches, { "origin", "upstream" }) end) t.test("complete push second positional returns refs", function() local dir = make_repo({ a = "x" }) - git(dir, "branch", "feature") + h.git(dir, "branch", "feature") local matches = cmd.complete("", "G push origin ", 14) eq_sorted(matches, { "HEAD", "feature", "main" }) end) @@ -203,9 +198,9 @@ t.test("complete add returns only unstaged/untracked paths", function() t.write(dir, "newfile", "new") local r = assert(require("git.repo").resolve(dir)) r:refresh() - vim.wait(500, function() + t.wait_for(function() return r.status and #vim.tbl_keys(r.status.entries) > 0 - end) + end, "git status to report entries", 500) local matches = cmd.complete("", "G add ", 6) eq_sorted(matches, { "newfile", "tracked" }) end) @@ -218,19 +213,19 @@ t.test("complete after `--` returns tracked paths only", function() end) t.test("complete stash returns subsubcommands", function() - local dir = make_repo({ a = "x" }) + make_repo({ a = "x" }) local matches = cmd.complete("p", "G stash p", 9) eq_sorted(matches, { "pop", "push" }) end) t.test("complete show with : returns tree paths", function() - local dir = make_repo({ a = "x", ["sub/b"] = "y" }) + make_repo({ a = "x", ["sub/b"] = "y" }) local matches = cmd.complete("HEAD:", "G show HEAD:", 12) eq_sorted(matches, { "HEAD:a", "HEAD:sub/" }) end) t.test("complete unknown subcommand falls back to tracked paths", function() - local dir = make_repo({ a = "x", b = "y" }) + make_repo({ a = "x", b = "y" }) local matches = cmd.complete("", "G nonexistent ", 14) eq_sorted(matches, { "a", "b" }) end) @@ -250,14 +245,35 @@ end ---@param buf_name_pattern string ---@param timeout integer? local function wait_buf_populated(buf_name_pattern, timeout) - vim.wait(timeout or 1000, function() + t.wait_for(function() for _, b in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_get_name(b):match(buf_name_pattern) then return #vim.api.nvim_buf_get_lines(b, 0, -1, false) > 1 end end return false - end) + end, "buffer matching " .. buf_name_pattern .. " to populate", timeout) +end + +---Wait for a buffer matching `buf_name_pattern` to contain a line whose +---content equals `line`. Useful for asserting that re-running a :G +---command repopulated the buffer with new output. +---@param buf_name_pattern string +---@param line string +---@param timeout integer? +local function wait_buf_has_line(buf_name_pattern, line, timeout) + t.wait_for(function() + for _, b in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_get_name(b):match(buf_name_pattern) then + for _, l in ipairs(vim.api.nvim_buf_get_lines(b, 0, -1, false)) do + if l == line then + return true + end + end + end + end + return false + end, "buffer " .. buf_name_pattern .. " to contain " .. line, timeout) end t.test("run :G diff reuses the same buffer across invocations", function() @@ -265,17 +281,17 @@ t.test("run :G diff reuses the same buffer across invocations", function() t.write(dir, "a", "v2\n") cmd.run({ "diff" }) - wait_buf_populated("%[Git diff%]") - local first_count = count_bufs_named("%[Git diff%]") - t.eq(first_count, 1) + wait_buf_has_line("%[Git diff%]", "+v2") + t.eq(count_bufs_named("%[Git diff%]"), 1) t.write(dir, "a", "v3\n") cmd.run({ "diff" }) - vim.wait(300) + wait_buf_has_line("%[Git diff%]", "+v3") t.eq(count_bufs_named("%[Git diff%]"), 1, "second :G diff should reuse") + t.write(dir, "a", "v4\n") cmd.run({ "diff" }) - vim.wait(300) + wait_buf_has_line("%[Git diff%]", "+v4") t.eq(count_bufs_named("%[Git diff%]"), 1, "third :G diff should reuse") end) @@ -293,13 +309,14 @@ end t.test(":G show on + line opens the blob URI", function() local dir = make_repo({ a = "first\n" }) t.write(dir, "a", "second\n") - git(dir, "add", "a") - git(dir, "commit", "-q", "-m", "second") - local r = assert(require("git.repo").resolve(dir)) - local blob = vim.trim(git(dir, "rev-parse", "HEAD:a").stdout) + h.git(dir, "add", "a") + h.git(dir, "commit", "-q", "-m", "second") + assert(require("git.repo").resolve(dir)) + local blob = h.git(dir, "rev-parse", "HEAD:a").stdout cmd.run({ "show", "HEAD" }) wait_buf_populated("%[Git show HEAD%]") + ---@type integer? local diff_buf for _, b in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_get_name(b):match("%[Git show HEAD%]") then @@ -319,30 +336,23 @@ end) t.test("gl log buffer refills after jumping back", function() local dir = make_repo({ a = "v1\n" }) t.write(dir, "a", "v2\n") - helpers.git(dir, "add", "a") - helpers.git(dir, "commit", "-q", "-m", "second") + h.git(dir, "add", "a") + h.git(dir, "commit", "-q", "-m", "second") require("git.log_view").open({ max_count = 1000 }) wait_buf_populated("^gitlog://") local log_buf = vim.api.nvim_get_current_buf() local log_win = vim.api.nvim_get_current_win() - t.truthy( - vim.api.nvim_buf_get_name(log_buf):match("^gitlog://") - ) - local initial_lines = - #vim.api.nvim_buf_get_lines(log_buf, 0, -1, false) + t.truthy(vim.api.nvim_buf_get_name(log_buf):match("^gitlog://")) + local initial_lines = #vim.api.nvim_buf_get_lines(log_buf, 0, -1, false) t.truthy(initial_lines >= 2) -- Step into a commit, then back to the log. vim.api.nvim_win_set_cursor(log_win, { 1, 0 }) - local cr = - vim.api.nvim_replace_termcodes("", true, false, true) - vim.api.nvim_feedkeys(cr, "x", false) + t.press("") t.truthy(vim.api.nvim_buf_get_name(0):match("^git://")) - local co = - vim.api.nvim_replace_termcodes("", true, false, true) - vim.api.nvim_feedkeys(co, "x", false) + t.press("") t.eq(vim.api.nvim_get_current_buf(), log_buf) t.eq( #vim.api.nvim_buf_get_lines(log_buf, 0, -1, false), @@ -354,8 +364,8 @@ end) t.test(" still dispatches after navigating away and back", function() local dir = make_repo({ a = "v1\n" }) t.write(dir, "a", "v2\n") - helpers.git(dir, "add", "a") - helpers.git(dir, "commit", "-q", "-m", "second") + h.git(dir, "add", "a") + h.git(dir, "commit", "-q", "-m", "second") -- Open the HEAD commit object buffer. Its cat-file output includes a -- "parent " line we can navigate from. @@ -376,8 +386,7 @@ t.test(" still dispatches after navigating away and back", function() -- back to first_obj_buf. With bufhidden=delete, vim re-reads the -- URI, which previously raced with BufDelete-driven unbind and left -- state cleared, so open_under_cursor returned false. - local co = vim.api.nvim_replace_termcodes("", true, false, true) - vim.api.nvim_feedkeys(co, "x", false) + t.press("") t.eq(vim.api.nvim_get_current_buf(), first_obj_buf) local tree_lnum = assert(find_line(first_obj_buf, "tree ")) vim.api.nvim_win_set_cursor(first_obj_win, { tree_lnum, 0 }) @@ -390,10 +399,11 @@ end) t.test(":G diff on + line falls back to worktree file", function() local dir = make_repo({ a = "v1\n" }) t.write(dir, "a", "v2\n") - local r = assert(require("git.repo").resolve(dir)) + assert(require("git.repo").resolve(dir)) cmd.run({ "diff" }) wait_buf_populated("%[Git diff%]") + ---@type integer? local diff_buf for _, b in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_get_name(b):match("%[Git diff%]") then diff --git a/test/git/helpers.lua b/test/git/helpers.lua index 0730671..f01a7c4 100644 --- a/test/git/helpers.lua +++ b/test/git/helpers.lua @@ -2,9 +2,11 @@ local t = require("test") local M = {} +---@class test.git.SystemCompleted : vim.SystemCompleted +---@field stdout string + ---@param dir string ----@vararg string ----@return vim.SystemCompleted +---@return test.git.SystemCompleted function M.git(dir, ...) local r = vim.system({ "git", ... }, { cwd = dir, text = true }):wait() if r.code ~= 0 then @@ -17,6 +19,11 @@ function M.git(dir, ...) 2 ) end + if r.stdout then + r.stdout = vim.trim(r.stdout) + else + r.stdout = "" + end return r end diff --git a/test/git/repo_test.lua b/test/git/repo_test.lua index 0479f28..191c4f4 100644 --- a/test/git/repo_test.lua +++ b/test/git/repo_test.lua @@ -1,24 +1,22 @@ +---@diagnostic disable: access-invisible +local h = require("test.git.helpers") local t = require("test") -local helpers = require("test.git.helpers") require("git").init() -local git = helpers.git -local make_repo = helpers.make_repo - ---@param r ow.Git.Repo ---@param key string ---@param timeout integer? local function wait_cleared(r, key, timeout) - vim.wait(timeout or 2000, function() + t.wait_for(function() return r._cache[key] == nil - end) + end, key .. " cache to clear", timeout or 2000) end t.test("list_refs returns heads, tags, remotes (no HEAD)", function() - local dir = make_repo({ a = "x" }) - git(dir, "branch", "feature") - git(dir, "tag", "v1") + local dir = h.make_repo({ a = "x" }) + h.git(dir, "branch", "feature") + h.git(dir, "tag", "v1") local r = assert(require("git.repo").resolve(dir)) local refs = r:list_refs() table.sort(refs) @@ -26,13 +24,13 @@ t.test("list_refs returns heads, tags, remotes (no HEAD)", function() end) t.test("list_pseudo_refs always includes HEAD", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) t.eq(r:list_pseudo_refs(), { "HEAD" }) end) t.test("list_pseudo_refs picks up MERGE_HEAD when present", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) t.write(dir .. "/.git", "MERGE_HEAD", "deadbeef\n") -- Bypass cache (file appeared after first scan). @@ -43,15 +41,15 @@ t.test("list_pseudo_refs picks up MERGE_HEAD when present", function() end) t.test("list_stash_refs is empty when no stash", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) t.eq(r:list_stash_refs(), {}) end) t.test("list_stash_refs lists stash + entries when stash exists", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) t.write(dir, "a", "modified") - git(dir, "stash") + h.git(dir, "stash") local r = assert(require("git.repo").resolve(dir)) local refs = r:list_stash_refs() t.eq(#refs, 2) @@ -60,7 +58,7 @@ t.test("list_stash_refs lists stash + entries when stash exists", function() end) t.test("get_cached memoizes by key", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) local calls = 0 local v1 = r:get_cached("k", function() @@ -76,23 +74,23 @@ t.test("get_cached memoizes by key", function() end) t.test("cache clears after top-level .git change (commit)", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) local _ = r:list_refs() t.truthy(r._cache.refs) t.write(dir, "b", "y") - git(dir, "add", "b") - git(dir, "commit", "-q", "-m", "two") + h.git(dir, "add", "b") + h.git(dir, "commit", "-q", "-m", "two") wait_cleared(r, "refs") t.falsy(r._cache.refs, "cache should be cleared after commit") end) t.test("cache clears after slash-branch creation (polyfill)", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) local _ = r:list_refs() t.truthy(r._cache.refs) - git(dir, "branch", "feat/foo") + h.git(dir, "branch", "feat/foo") wait_cleared(r, "refs") t.falsy(r._cache.refs, "cache should clear via polyfilled subdir watcher") local refs = r:list_refs() @@ -101,10 +99,10 @@ t.test("cache clears after slash-branch creation (polyfill)", function() end) t.test("cache clears after deeply nested slash branch", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) local _ = r:list_refs() - git(dir, "branch", "deep/a/b/c") + h.git(dir, "branch", "deep/a/b/c") wait_cleared(r, "refs") local refs = r:list_refs() table.sort(refs) @@ -112,9 +110,9 @@ t.test("cache clears after deeply nested slash branch", function() end) t.test("resolve_sha returns ok + full sha for a known blob", function() - local dir = make_repo({ a = "hello\n" }) + local dir = h.make_repo({ a = "hello\n" }) local r = assert(require("git.repo").resolve(dir)) - local blob = vim.trim(git(dir, "rev-parse", "HEAD:a").stdout) + local blob = h.git(dir, "rev-parse", "HEAD:a").stdout local short = blob:sub(1, 7) local full, status = r:resolve_sha(short) t.eq(status, "ok") @@ -122,7 +120,7 @@ t.test("resolve_sha returns ok + full sha for a known blob", function() end) t.test("resolve_sha returns missing for an unknown prefix", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) local full, status = r:resolve_sha("0000deadbeef") t.eq(full, nil) @@ -130,29 +128,29 @@ t.test("resolve_sha returns missing for an unknown prefix", function() end) t.test("resolve_sha caches by prefix", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) - local blob = vim.trim(git(dir, "rev-parse", "HEAD:a").stdout) + local blob = h.git(dir, "rev-parse", "HEAD:a").stdout local short = blob:sub(1, 7) local _, _ = r:resolve_sha(short) t.truthy(r._cache["resolve:" .. short], "result should be cached") end) t.test("watcher cleans up after a slash-branch dir is removed", function() - local dir = make_repo({ a = "x" }) + local dir = h.make_repo({ a = "x" }) local r = assert(require("git.repo").resolve(dir)) - git(dir, "branch", "feat/foo") + h.git(dir, "branch", "feat/foo") -- Wait for the dynamic watcher on .git/refs/heads/feat to be added. local feat_path = dir .. "/.git/refs/heads/feat" - vim.wait(2000, function() + t.wait_for(function() return r._watchers[feat_path] ~= nil - end) + end, "watcher to be installed on feat/ subdir", 2000) t.truthy(r._watchers[feat_path], "feat/ subdir should be watched") -- Remove the branch; the feat/ directory becomes empty and is -- pruned by git, triggering the deleted-self event. - git(dir, "branch", "-D", "feat/foo") - vim.wait(2000, function() + h.git(dir, "branch", "-D", "feat/foo") + t.wait_for(function() return r._watchers[feat_path] == nil - end) + end, "watcher on feat/ subdir to close", 2000) t.falsy(r._watchers[feat_path], "watcher should self-close") end) diff --git a/test/git/status_view_test.lua b/test/git/status_view_test.lua index bc1b933..7e7ecd1 100644 --- a/test/git/status_view_test.lua +++ b/test/git/status_view_test.lua @@ -1,11 +1,8 @@ +local h = require("test.git.helpers") local t = require("test") -local helpers = require("test.git.helpers") require("git").init() -local git = helpers.git -local make_repo = helpers.make_repo - ---Replicate the user's global cursor-restore autocmd. Scoped to a ---named augroup + cleanup so it doesn't leak between tests. local function install_cursor_restore_autocmd() @@ -32,26 +29,24 @@ local function find_line(sidebar_buf, needle) end end ----@return integer? sidebar_buf, integer? sidebar_win +---Find the gitstatus sidebar window in the current tabpage. +---@return integer? sidebar_buf +---@return integer? sidebar_win local function find_sidebar() - local buf, win - for _, b in ipairs(vim.api.nvim_list_bufs()) do + for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local b = vim.api.nvim_win_get_buf(w) if vim.bo[b].filetype == "gitstatus" then - buf = b + return b, w end end - for _, w in ipairs(vim.api.nvim_list_wins()) do - if vim.api.nvim_win_get_buf(w) == buf then - win = w - end - end - return buf, win end ----@param role string +---Find a diff-role window in the given tabpage (or current). +---@param role "left"|"right" +---@param tab integer? ---@return integer? -local function find_diff_win(role) - for _, w in ipairs(vim.api.nvim_list_wins()) do +local function find_diff_win(role, tab) + for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tab or 0)) do if vim.w[w].git_diff_role == role then return w end @@ -68,7 +63,7 @@ local function setup_sidebar_with_unstaged_file( committed_content, worktree_content ) - local repo = make_repo({ [file_path] = committed_content }) + local repo = h.make_repo({ [file_path] = committed_content }) t.write(repo, file_path, worktree_content) vim.cmd("cd " .. repo) @@ -82,9 +77,9 @@ local function setup_sidebar_with_unstaged_file( "repo should resolve for the test worktree" ) r:refresh() - vim.wait(1000, function() + t.wait_for(function() return r.status and #r.status:by_kind("unstaged") > 0 - end) + end, "git status to report unstaged changes") local entry_line = assert( find_line(sidebar_buf, vim.pesc(file_path) .. "$"), @@ -93,17 +88,6 @@ local function setup_sidebar_with_unstaged_file( return sidebar_win, entry_line end -local function press(keys) - local rhs = vim.api.nvim_replace_termcodes(keys, true, false, true) - vim.api.nvim_feedkeys(rhs, "x", false) -end - ----@param cond fun(): boolean ----@param msg string -local function wait_for(cond, msg) - t.truthy(vim.wait(1000, cond), "timed out waiting for: " .. msg) -end - t.test("stage with diff open: sidebar cursor stays put", function() install_cursor_restore_autocmd() local sidebar_win, line = setup_sidebar_with_unstaged_file( @@ -115,15 +99,15 @@ t.test("stage with diff open: sidebar cursor stays put", function() vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) - press("") - wait_for(function() + t.press("") + t.wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") local r = assert(require("git.repo").find(vim.fn.getcwd())) vim.api.nvim_set_current_win(sidebar_win) - press("s") - wait_for(function() + t.press("s") + t.wait_for(function() return #r.status:by_kind("staged") > 0 end, "stage to propagate to repo state") @@ -146,8 +130,8 @@ t.test( vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) - press("") - wait_for(function() + t.press("") + t.wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") local left_win = assert(find_diff_win("left")) @@ -159,8 +143,8 @@ t.test( local r = assert(require("git.repo").find(vim.fn.getcwd())) vim.api.nvim_set_current_win(sidebar_win) - press("s") - wait_for(function() + t.press("s") + t.wait_for(function() return #r.status:by_kind("staged") > 0 end, "stage to propagate to repo state") @@ -172,14 +156,51 @@ t.test( end ) +t.test( + " in a second tabpage opens the diff inside that tabpage", + function() + local sidebar_win, line = + setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n") + local tab1 = vim.api.nvim_get_current_tabpage() + + -- First show diff in tab1, so state.diff_*_win point at tab1. + vim.api.nvim_set_current_win(sidebar_win) + vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) + t.press("") + t.wait_for(function() + return find_diff_win("left", tab1) ~= nil + end, "diff windows in tab1 to appear") + + vim.cmd("tabnew") + require("git.status_view").open({ placement = "sidebar" }) + local tab2 = vim.api.nvim_get_current_tabpage() + t.truthy(tab2 ~= tab1, "tabnew should produce a distinct tabpage") + + local _, sidebar_win2 = find_sidebar() + assert(sidebar_win2, "sidebar window should exist in tab2") + vim.api.nvim_set_current_win(sidebar_win2) + vim.api.nvim_win_set_cursor(sidebar_win2, { line, 0 }) + + t.press("") + t.wait_for(function() + return find_diff_win("left", tab2) ~= nil + end, "diff windows in tab2 to appear") + + t.truthy( + find_diff_win("right", tab2), + "right diff window should be in tab2" + ) + end +) + t.test("refresh on stage updates the index URI buffer's content", function() local sidebar_win, line = setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n") vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) - press("") - wait_for(function() + t.press("") + t.wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") @@ -192,8 +213,8 @@ t.test("refresh on stage updates the index URI buffer's content", function() ) vim.api.nvim_set_current_win(sidebar_win) - press("s") - wait_for(function() + t.press("s") + t.wait_for(function() local first = vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1] return first == "v2" end, "index pane to refresh to staged content") diff --git a/test/init.lua b/test/init.lua index 2708bc2..17c91b2 100644 --- a/test/init.lua +++ b/test/init.lua @@ -138,10 +138,27 @@ end ---@param content string function M.write(dir, path, content) local full = vim.fs.joinpath(dir, path) - vim.fn.mkdir(vim.fs.dirname(full), "p") - local f = assert(io.open(full --[[@as string]], "w")) + local parent = vim.fs.dirname(full) + vim.fn.mkdir(parent, "p") + local f, err = io.open(full, "w") + if not f then + error(err or "io.open failed: " .. full) + end f:write(content) f:close() end +---@param keys string +function M.press(keys) + local rhs = vim.api.nvim_replace_termcodes(keys, true, false, true) + vim.api.nvim_feedkeys(rhs, "x", false) +end + +---@param cond fun(): boolean +---@param msg string +---@param timeout integer? +function M.wait_for(cond, msg, timeout) + M.truthy(vim.wait(timeout or 1000, cond), "timed out waiting for: " .. msg) +end + return M diff --git a/test/runner.lua b/test/runner.lua index f2a3825..16b2b20 100644 --- a/test/runner.lua +++ b/test/runner.lua @@ -1,4 +1,7 @@ local cfg = vim.fn.stdpath("config") +if type(cfg) == "table" then + cfg = assert(cfg[1]) +end package.path = package.path .. (";" .. cfg .. "/lua/?.lua") .. (";" .. cfg .. "/lua/?/init.lua") @@ -14,6 +17,8 @@ end local t = require("test") +---@param target string +---@return string[]? local function gather(target) local abs = vim.fn.fnamemodify(target, ":p") if vim.fn.isdirectory(abs) == 1 then @@ -24,9 +29,8 @@ local function gather(target) end end -local args = arg or {} -local targets = #args > 0 and args or { cfg .. "/test" } - +local targets = (arg and #arg > 0) and arg or { cfg .. "/test" } +---@type string[] local files = {} local resolve_failed = false for _, target in ipairs(targets) do @@ -44,8 +48,10 @@ table.sort(files) local total_passed, total_failed = 0, 0 for _, f in ipairs(files) do - f = f --[[@as string]] - local label = f:sub(1, #cfg + 1) == cfg .. "/" and f:sub(#cfg + 2) or f + local label = f + if f:sub(1, #cfg + 1) == cfg .. "/" then + label = f:sub(#cfg + 2) + end t.start_file(label) local ok, err = pcall(dofile, f) if not ok then