From 84ee0bf014f6d4692c60d30103063ed93ea7cafd Mon Sep 17 00:00:00 2001 From: Simon McLean Date: Thu, 12 Sep 2024 18:49:17 +0100 Subject: [PATCH] Implement custom test framework (#63) * WIP * first test working * typo * update test playground files * rolling my own test runner * create debug logger * create debug logger * a bit of cleanup * 4 tests working * more WIP * test framework beta ready * unit tests * help spec * various things * typo * add TEST_README.md * update test readme * almost there * implement global test queue * move test framework into its own directory * delete submodules * update validate.sh * formatting --------- Co-authored-by: Simon McLean --- .gitignore | 1 + lib/busted | 1 - lib/luassert | 1 - lua/triptych/actions.lua | 4 +- lua/triptych/autocmds.lua | 5 +- lua/triptych/config.lua | 1 + lua/triptych/diagnostics.lua | 1 - lua/triptych/event_handlers.lua | 15 +- lua/triptych/float.lua | 11 - lua/triptych/fs.lua | 2 - lua/triptych/git.lua | 1 - lua/triptych/help.lua | 1 - lua/triptych/init.lua | 4 - lua/triptych/logger.lua | 13 + lua/triptych/mappings.lua | 1 - lua/triptych/syntax_highlighting.lua | 3 - lua/triptych/types.lua | 2 + lua/triptych/view.lua | 11 - test_framework/queue.lua | 162 ++++++ test_framework/test.lua | 154 ++++++ test_framework/utils.lua | 61 +++ tests/TESTS_README.md | 24 + tests/run_specs.lua | 49 ++ {unit_tests => tests/specs}/config_spec.lua | 18 +- tests/specs/help_spec.lua | 32 ++ tests/specs/ui_spec.lua | 492 ++++++++++++++++++ {unit_tests => tests/specs}/utils_spec.lua | 28 +- .../level_1/level_1_file_1.lua | 1 + .../level_1/level_2/level_2_file_1.lua | 1 + .../level_1/level_2/level_3/.hidden_dot_file | 0 .../level_1/level_2/level_3/level_3_file_1.md | 3 + .../level_3/level_4/level_4_file_1.lua | 5 + .../level_4/level_5/level_5_file_1.lua | 3 + tests/utils.lua | 218 ++++++++ ui_tests/setup.lua | 94 ---- ui_tests/tests.lua | 212 -------- ui_tests/utils.lua | 57 -- unit_tests/autocmds_spec.lua | 39 -- unit_tests/diagnostics_spec.lua | 58 --- unit_tests/event_handlers_spec.lua | 47 -- unit_tests/fs_spec.lua | 31 -- unit_tests/git_spec.lua | 96 ---- unit_tests/help_spec.lua | 47 -- unit_tests/init_spec.lua | 327 ------------ unit_tests/mappings_spec.lua | 117 ----- unit_tests/state_spec.lua | 188 ------- unit_tests/test_utils.lua | 25 - validate.sh | 20 +- 48 files changed, 1271 insertions(+), 1416 deletions(-) delete mode 160000 lib/busted delete mode 160000 lib/luassert create mode 100644 lua/triptych/logger.lua create mode 100644 test_framework/queue.lua create mode 100644 test_framework/test.lua create mode 100644 test_framework/utils.lua create mode 100644 tests/TESTS_README.md create mode 100644 tests/run_specs.lua rename {unit_tests => tests/specs}/config_spec.lua (89%) create mode 100644 tests/specs/help_spec.lua create mode 100644 tests/specs/ui_spec.lua rename {unit_tests => tests/specs}/utils_spec.lua (84%) create mode 100644 tests/test_playground/level_1/level_1_file_1.lua create mode 100644 tests/test_playground/level_1/level_2/level_2_file_1.lua create mode 100644 tests/test_playground/level_1/level_2/level_3/.hidden_dot_file create mode 100644 tests/test_playground/level_1/level_2/level_3/level_3_file_1.md create mode 100644 tests/test_playground/level_1/level_2/level_3/level_4/level_4_file_1.lua create mode 100644 tests/test_playground/level_1/level_2/level_3/level_4/level_5/level_5_file_1.lua create mode 100644 tests/utils.lua delete mode 100644 ui_tests/setup.lua delete mode 100644 ui_tests/tests.lua delete mode 100644 ui_tests/utils.lua delete mode 100644 unit_tests/autocmds_spec.lua delete mode 100644 unit_tests/diagnostics_spec.lua delete mode 100644 unit_tests/event_handlers_spec.lua delete mode 100644 unit_tests/fs_spec.lua delete mode 100644 unit_tests/git_spec.lua delete mode 100644 unit_tests/help_spec.lua delete mode 100644 unit_tests/init_spec.lua delete mode 100644 unit_tests/mappings_spec.lua delete mode 100644 unit_tests/state_spec.lua delete mode 100644 unit_tests/test_utils.lua diff --git a/.gitignore b/.gitignore index e43b0f9..c5c3461 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +git_ignored_file diff --git a/lib/busted b/lib/busted deleted file mode 160000 index 5ed85d0..0000000 --- a/lib/busted +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ed85d0e016a5eb5eca097aa52905eedf1b180f1 diff --git a/lib/luassert b/lib/luassert deleted file mode 160000 index d3528bb..0000000 --- a/lib/luassert +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d3528bb679302cbfdedefabb37064515ab95f7b9 diff --git a/lua/triptych/actions.lua b/lua/triptych/actions.lua index 0f69033..f4d32e5 100644 --- a/lua/triptych/actions.lua +++ b/lua/triptych/actions.lua @@ -48,8 +48,6 @@ end ---@param State TriptychState ---@param refresh_view fun(): nil function Actions.new(State, refresh_view) - local vim = _G.triptych_mock_vim or vim - local M = {} ---@return nil @@ -308,7 +306,7 @@ function Actions.new(State, refresh_view) for _, item in ipairs(State.copy_list) do local destination = get_copy_path(u.path_join(destination_dir, item.display_name)) M.duplicate_file_or_dir(item, destination) - autocmds.publish_file_created(destination) + autocmds.publish_did_delete_node(destination) end view.jump_cursor_to(State, destination_dir) diff --git a/lua/triptych/autocmds.lua b/lua/triptych/autocmds.lua index df47ec6..d9eac0f 100644 --- a/lua/triptych/autocmds.lua +++ b/lua/triptych/autocmds.lua @@ -1,3 +1,5 @@ +local log = require 'triptych.logger' + local au_group_internal = vim.api.nvim_create_augroup('TriptychEventsInternal', { clear = true }) local au_group_public = vim.api.nvim_create_augroup('TriptychEvents', { clear = true }) @@ -11,7 +13,6 @@ local AutoCommands = {} ---@param Git? Git ---@return AutoCommands function AutoCommands.new(event_handlers, State, Diagnostics, Git) - local vim = _G.triptych_mock_vim or vim local instance = {} setmetatable(instance, { __index = AutoCommands }) @@ -55,7 +56,6 @@ end M.new = AutoCommands.new function AutoCommands:destroy_autocommands() - local vim = _G.triptych_mock_vim or vim for _, autocmd in pairs(self.autocmds) do vim.api.nvim_del_autocmd(autocmd) end @@ -125,6 +125,7 @@ end ---@param win_type WinType function M.publish_did_update_window(win_type) + log.debug('publish_did_update_window', { win_type = win_type }) exec_public_autocmd('TriptychDidUpdateWindow', { win_type = win_type, }) diff --git a/lua/triptych/config.lua b/lua/triptych/config.lua index 7697372..680a003 100644 --- a/lua/triptych/config.lua +++ b/lua/triptych/config.lua @@ -9,6 +9,7 @@ end ---@return TriptychConfig local function default_config() return { + debug = false, mappings = { -- Everything below is buffer-local, meaning it will only apply to Triptych windows show_help = 'g?', diff --git a/lua/triptych/diagnostics.lua b/lua/triptych/diagnostics.lua index 06b463f..8ae24ea 100644 --- a/lua/triptych/diagnostics.lua +++ b/lua/triptych/diagnostics.lua @@ -13,7 +13,6 @@ Diagnostics.get_sign = function(severity) end Diagnostics.new = function() - local vim = _G.triptych_mock_vim or vim local instance = {} setmetatable(instance, { __index = Diagnostics }) diff --git a/lua/triptych/event_handlers.lua b/lua/triptych/event_handlers.lua index d631fe6..f3bbfc7 100644 --- a/lua/triptych/event_handlers.lua +++ b/lua/triptych/event_handlers.lua @@ -1,6 +1,8 @@ local u = require 'triptych.utils' +local log = require 'triptych.logger' local float = require 'triptych.float' local autocmds = require 'triptych.autocmds' +local view = require 'triptych.view' local M = {} @@ -8,8 +10,7 @@ local M = {} ---@param State TriptychState ---@return nil function M.handle_cursor_moved(State) - local vim = _G.triptych_mock_vim or vim - local view = _G.triptych_mock_view or require 'triptych.view' + log.debug 'handle_cursor_moved' local target = view.get_target_under_cursor(State) local current_dir = State.windows.current.path local line_number = vim.api.nvim_win_get_cursor(0)[1] @@ -42,8 +43,7 @@ end ---@param Git? Git ---@return nil function M.handle_dir_read(State, path_details, win_type, Diagnostics, Git) - local vim = _G.triptych_mock_vim or vim - local view = _G.triptych_mock_view or require 'triptych.view' + log.debug('handle_cursor_moved', { win_type = win_type }) view.set_parent_or_primary_window_lines(State, path_details, win_type, Diagnostics, Git) -- Set cursor position @@ -79,8 +79,11 @@ end ---@param path string ---@param lines string[] function M.handle_file_read(child_win_buf, path, lines) - float.set_child_window_file_preview(child_win_buf, path, lines) - autocmds.publish_did_update_window 'child' + log.debug('handle_file_read', { path = path }) + if vim.g.triptych_is_open then + float.set_child_window_file_preview(child_win_buf, path, lines) + autocmds.publish_did_update_window 'child' + end end ---@return nil diff --git a/lua/triptych/float.lua b/lua/triptych/float.lua index 52753a9..0198c07 100644 --- a/lua/triptych/float.lua +++ b/lua/triptych/float.lua @@ -9,7 +9,6 @@ local M = {} ---@param fn fun(): nil ---@return nil local function modify_locked_buffer(buf, fn) - local vim = _G.triptych_mock_vim or vim vim.api.nvim_buf_set_option(buf, 'readonly', false) vim.api.nvim_buf_set_option(buf, 'modifiable', true) fn() @@ -21,7 +20,6 @@ end ---@param lines string[] ---@return nil function M.buf_set_lines(buf, lines) - local vim = _G.triptych_mock_vim or vim modify_locked_buffer(buf, function() vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) end) @@ -32,7 +30,6 @@ end ---@param highlights HighlightDetails[] ---@return nil function M.buf_apply_highlights(buf, highlights) - local vim = _G.triptych_mock_vim or vim -- Apply icon highlight for i, highlight in ipairs(highlights) do vim.api.nvim_buf_add_highlight(buf, 0, highlight.icon.highlight_name, i - 1, 0, highlight.icon.length) @@ -46,7 +43,6 @@ end ---@param lines string[] ---@param attempt_scroll_top? boolean function M.win_set_lines(win, lines, attempt_scroll_top) - local vim = _G.triptych_mock_vim or vim local buf = vim.api.nvim_win_get_buf(win) M.buf_set_lines(buf, lines) if attempt_scroll_top then @@ -63,7 +59,6 @@ end ---@param postfix? string ---@return nil function M.win_set_title(win, title, icon, highlight, postfix) - local vim = _G.triptych_mock_vim or vim vim.api.nvim_win_call(win, function() local maybe_icon = '' if vim.g.triptych_config.options.file_icons.enabled and icon then @@ -96,7 +91,6 @@ end ---@return number local function create_new_buffer() - local vim = _G.triptych_mock_vim or vim local buf = vim.api.nvim_create_buf(false, true) return buf end @@ -104,7 +98,6 @@ end ---@param config FloatingWindowConfig ---@return number local function create_floating_window(config) - local vim = _G.triptych_mock_vim or vim local buf = create_new_buffer() local win = vim.api.nvim_open_win(buf, true, { width = config.width, @@ -132,7 +125,6 @@ end ---@param winblend number ---@return number local function create_backdrop(winblend) - local vim = _G.triptych_mock_vim or vim local buf = create_new_buffer() local win = vim.api.nvim_open_win(buf, false, { width = vim.o.columns, @@ -174,8 +166,6 @@ function M.create_three_floating_windows( margin_x, margin_y ) - local vim = _G.triptych_mock_vim or vim - local screen_height = vim.o.lines local screen_width = vim.o.columns @@ -304,7 +294,6 @@ end ---@param wins number[] ---@return nil function M.close_floats(wins) - local vim = _G.triptych_mock_vim or vim for _, win in ipairs(wins) do local buf = vim.api.nvim_win_get_buf(win) vim.api.nvim_buf_delete(buf, { force = true }) diff --git a/lua/triptych/fs.lua b/lua/triptych/fs.lua index 7bcec32..8c19b59 100644 --- a/lua/triptych/fs.lua +++ b/lua/triptych/fs.lua @@ -8,7 +8,6 @@ local M = {} ---@param path string ---@return number function M.get_file_size_in_kb(path) - local vim = _G.triptych_mock_vim or vim local bytes = vim.fn.getfsize(path) return bytes / 1000 end @@ -43,7 +42,6 @@ end, 2) ---@param show_hidden boolean ---@param callback fun(path_details: PathDetails): nil function M.get_path_details(_path, show_hidden, callback) - local vim = _G.triptych_mock_vim or vim local path = vim.fs.normalize(_path) local tree = { diff --git a/lua/triptych/git.lua b/lua/triptych/git.lua index 156f74b..82d93d3 100644 --- a/lua/triptych/git.lua +++ b/lua/triptych/git.lua @@ -44,7 +44,6 @@ end M.Git = {} function M.Git.new() - local vim = _G.triptych_mock_vim or vim local instance = {} setmetatable(instance, { __index = M.Git }) diff --git a/lua/triptych/help.lua b/lua/triptych/help.lua index 90749a0..818e403 100644 --- a/lua/triptych/help.lua +++ b/lua/triptych/help.lua @@ -2,7 +2,6 @@ ---@return string[] local function help_lines() - local vim = _G.triptych_mock_vim or vim local mappings = vim.g.triptych_config.mappings local lines = {} local left_col_length = 0 -- Used for padding and alignment diff --git a/lua/triptych/init.lua b/lua/triptych/init.lua index c361531..d6a5626 100644 --- a/lua/triptych/init.lua +++ b/lua/triptych/init.lua @@ -18,8 +18,6 @@ end ---@param dir? string Path of directory to open. If omitted will be the directory containing the current buffer ---@return fun()|nil local function toggle_triptych(dir) - local vim = _G.triptych_mock_vim or vim - if dir and not vim.fn.isdirectory(dir) then return warn(tostring(dir) .. ' is not a directory') end @@ -142,8 +140,6 @@ end ---@param user_config? table local function setup(user_config) - local vim = _G.triptych_mock_vim or vim - if vim.fn.has 'nvim-0.9.0' ~= 1 then return warn 'triptych.nvim requires Neovim >= 0.9.0' end diff --git a/lua/triptych/logger.lua b/lua/triptych/logger.lua new file mode 100644 index 0000000..8c9d7ab --- /dev/null +++ b/lua/triptych/logger.lua @@ -0,0 +1,13 @@ +return { + ---@param function_name string + ---@param data? table + debug = function(function_name, data) + if vim.g.triptych_config.debug then + local log_line = '[triptych][' .. function_name .. '] ' + if data then + log_line = log_line .. ' ' .. vim.inspect(data) + end + vim.print(log_line) + end + end, +} diff --git a/lua/triptych/mappings.lua b/lua/triptych/mappings.lua index 2b9ee12..f185094 100644 --- a/lua/triptych/mappings.lua +++ b/lua/triptych/mappings.lua @@ -6,7 +6,6 @@ local Mappings = {} ---@param actions unknown ---@param refresh_fn fun(): nil function Mappings.new(State, actions, refresh_fn) - local vim = _G.triptych_mock_vim or vim local mappings = vim.g.triptych_config.mappings local extension_mappings = vim.g.triptych_config.extension_mappings diff --git a/lua/triptych/syntax_highlighting.lua b/lua/triptych/syntax_highlighting.lua index feabaf8..caf173b 100644 --- a/lua/triptych/syntax_highlighting.lua +++ b/lua/triptych/syntax_highlighting.lua @@ -5,7 +5,6 @@ local M = {} ---@param buf number ---@return nil M.stop = function(buf) - local vim = _G.triptych_mock_vim or vim vim.treesitter.stop(buf) vim.api.nvim_buf_set_option(buf, 'syntax', 'off') end @@ -14,8 +13,6 @@ end ---@param filetype? string ---@return nil M.start = function(buf, filetype) - local vim = _G.triptych_mock_vim or vim - -- Because this function will be debounced we need to check that the buffer still exists if not vim.api.nvim_buf_is_valid(buf) then return diff --git a/lua/triptych/types.lua b/lua/triptych/types.lua index 9f6ce51..ae9b6a8 100644 --- a/lua/triptych/types.lua +++ b/lua/triptych/types.lua @@ -1,4 +1,6 @@ ---@class TriptychConfig +---@field debug boolean +---@field config boolean ---@field mappings TriptychConfigMappings ---@field extension_mappings { [string]: ExtensionMapping } ---@field options TriptychConfigOptions diff --git a/lua/triptych/view.lua b/lua/triptych/view.lua index 57e3484..a94acd3 100644 --- a/lua/triptych/view.lua +++ b/lua/triptych/view.lua @@ -70,7 +70,6 @@ end ---@return string[] # Lines including icons ---@return HighlightDetails[] local function path_details_to_lines(State, path_details) - local vim = _G.triptych_mock_vim or vim local config_options = vim.g.triptych_config.options local icons_enabled = config_options.file_icons.enabled local lines = {} @@ -163,7 +162,6 @@ end ---@param State TriptychState ---@return PathDetails? function M.get_target_under_cursor(State) - local vim = _G.triptych_mock_vim or vim local line_number = vim.api.nvim_win_get_cursor(0)[1] local contents = State.windows.current.contents if contents then @@ -175,7 +173,6 @@ end ---@param State TriptychState ---@return PathDetails[] function M.get_targets_in_selection(State) - local vim = _G.triptych_mock_vim or vim local from = vim.fn.getpos('v')[2] local to = vim.api.nvim_win_get_cursor(0)[1] local results = {} @@ -208,7 +205,6 @@ end ---@param path string ---@return string? local function get_title_postfix(path) - local vim = _G.triptych_mock_vim or vim if path == vim.fn.getcwd() then return '(cwd)' end @@ -230,7 +226,6 @@ end ---@param group string # see :h sign-group ---@return nil local function set_sign_columns(buf, children, group) - local vim = _G.triptych_mock_vim or vim vim.fn.sign_unplace(group) for index, entry in ipairs(children) do if entry.git_status then @@ -250,8 +245,6 @@ end ---@param target_dir string ---@return nil function M.set_primary_and_parent_window_targets(State, target_dir) - local vim = _G.triptych_mock_vim or vim - local focused_win = State.windows.current.win local parent_win = State.windows.parent.win local child_win = State.windows.child.win @@ -296,8 +289,6 @@ end ---@param Git? Git ---@return nil function M.set_parent_or_primary_window_lines(State, path_details, win_type, Diagnostics, Git) - local vim = _G.triptych_mock_vim or vim - local state = u.eval(function() if win_type == 'parent' then return State.windows.parent @@ -354,7 +345,6 @@ end ---@param path_details PathDetails ---@return nil function M.set_child_window_target(State, path_details) - local vim = _G.triptych_mock_vim or vim local buf = vim.api.nvim_win_get_buf(State.windows.child.win) -- TODO: Can we make path_details mandatory to avoid the repeated checks @@ -448,7 +438,6 @@ end ---@param path string ---@return nil function M.jump_cursor_to(State, path) - local vim = _G.triptych_mock_vim or vim local line_num for index, item in ipairs(State.windows.current.contents.children) do if item.path == path then diff --git a/test_framework/queue.lua b/test_framework/queue.lua new file mode 100644 index 0000000..2cfaee9 --- /dev/null +++ b/test_framework/queue.lua @@ -0,0 +1,162 @@ +local u = require 'test_framework.utils' + +local TIMEOUT_SECONDS = 7 + +---@class TestQueue +---@field queue Test[] +---@field completed Test[] +---@field is_running boolean +---@field add fun(self: TestQueue, test: Test) +---@field remove fun(self: TestQueue, id: string) +---@field run_next function +local TestQueue = {} + +function TestQueue.new() + local instance = {} + setmetatable(instance, { __index = TestQueue }) + instance.queue = {} + instance.completed = {} + return instance +end + +if not GlobalTestQueue then + ---@type TestQueue + GlobalTestQueue = TestQueue.new() +end + +function TestQueue:add(test) + table.insert(self.queue, test) + + if not self.is_running then + self.is_running = true + vim.schedule(function() + self:run_next() + end) + end +end + +---Cleanup that needs doing, whether the tests passed or failed +function TestQueue:cleanup() + self.is_running = false + for _, test in ipairs(self.queue) do + test:cleanup() + test.result = nil + end + for _, test in ipairs(self.completed) do + test:cleanup() + test.result = nil + end + self.queue = {} + self.completed = {} +end + +---On success (pass or skip) +function TestQueue:handle_all_tests_succeeded() + u.print '--- RESULTS ---' + + --Group tests by describe block + ---@type table + local grouped_tests = {} + for _, test in ipairs(self.completed) do + local descId = test.describe_id + if not grouped_tests[descId] then + grouped_tests[descId] = { + describe_title = test.describe_title, + tests = {}, + } + end + table.insert(grouped_tests[descId].tests, test) + end + + local result_count = { + passed = 0, + skipped = 0, + failed = 0, + } + + -- Print each the result + for _, describe_block in pairs(grouped_tests) do + u.print('[DESCRIBE] ' .. describe_block.describe_title) + for _, test in ipairs(describe_block.tests) do + result_count[test.result] = result_count[test.result] + 1 + u.print(' - [' .. string.upper(test.result) .. '] ' .. test.name) + end + end + + -- Print the summary + u.print( + 'Finished running ' + .. #self.completed + .. ' tests. ' + .. result_count.skipped + .. ' skipped, ' + .. result_count.passed + .. ' passed, 0 failed' + ) + + self:cleanup() + if u.is_headless() then + u.exit_status_code 'success' + end +end + +---@param test Test +---@param fail_message? string +function TestQueue:handle_test_fail(test, fail_message) + test.result = 'failed' + u.print('[FAILED] ' .. test.name) + error(fail_message) + self:cleanup() + if u.is_headless() then + u.exit_status_code 'failed' + else + end +end + +function TestQueue:run_next() + local test = self.queue[1] + + local function next_or_finish() + local completed_test = table.remove(self.queue, 1) + table.insert(self.completed, completed_test) + vim.schedule(function() + if #self.queue > 0 then + self:run_next() + else + self:handle_all_tests_succeeded() + end + end) + end + + if test.is_skipped then + u.print('[SKIPPING] ' .. test.describe_title .. ' - ' .. test.name) + test.result = 'skipped' + next_or_finish() + else + u.print('[RUNNING] ' .. test.describe_title .. ' - ' .. test.name) + + -- Timout timer + local timer = vim.loop.new_timer() + timer:start(1000 * TIMEOUT_SECONDS, 0, function() + test.is_timed_out = true + self:handle_test_fail(test, 'Timeout after ' .. TIMEOUT_SECONDS .. ' seconds') + end) + + test:run(function(passed, fail_message) + timer:stop() + if not passed then + self:handle_test_fail(test, fail_message) + else + test.result = 'passed' + next_or_finish() + end + end) + end +end + +return { + ---@param test Test + add = function(test) + GlobalTestQueue:add(test) + end, +} diff --git a/test_framework/test.lua b/test_framework/test.lua new file mode 100644 index 0000000..053a0a3 --- /dev/null +++ b/test_framework/test.lua @@ -0,0 +1,154 @@ +local u = require 'test_framework.utils' +local test_queue = require 'test_framework.queue' + +local M = {} + +---@alias AsyncTestCallback fun(done: { assertions: function, cleanup?: function }) + +---@class Test +---@field name string +---@field id string +---@field describe_id string +---@field describe_title string +---@field is_skipped boolean +---@field is_onlyed boolean +---@field is_timed_out boolean +---@field has_run boolean +---@field result? ('passed' | 'failed' | 'skipped') +---@field is_async boolean +---@field test_body? function +---@field test_body_async? AsyncTestCallback +local Test = {} + +---@param name string +---@return Test +function Test.new(name) + local instance = {} + setmetatable(instance, { __index = Test }) + + instance.name = name + instance.id = u.UUID() + instance.is_onlyed = false + instance.is_timed_out = false + instance.has_run = false + + return instance +end + +---Define a synchronous test +---@param name string +---@param test_body function +---@return Test +M.test = function(name, test_body) + local t = Test.new(name) + t.test_body = test_body + t.is_async = false + return t +end + +---Define an asyncronous test +---@param name string +---@param test_body AsyncTestCallback +---@return Test +M.test_async = function(name, test_body) + local t = Test.new(name) + t.test_body_async = test_body + t.is_async = true + return t +end + +---Run a test, calling the relevant method depending on whether it's sync or async +---@param callback fun(passed: boolean, fail_message?: string) +function Test:run(callback) + if self.is_async then + self:run_async(callback) + else + self:run_sync(callback) + end +end + +---Run an asyncronous test +---@param callback fun(passed: boolean, fail_message?: string) +function Test:run_async(callback) + local success, err = pcall(self.test_body_async, function(test_callback) + if self.has_run then + error 'Attempted to invoke test completion more than once. Check that any async callbacks in the test are not firing multiple times.' + end + + self.has_run = true + + if test_callback.cleanup then + local cleanup_successful, cleanup_err = pcall(test_callback.cleanup) + if not cleanup_successful then + error(cleanup_err) + end + end + + if self.is_timed_out then + callback(false, 'Timed out') + else + local passed, fail_message = pcall(test_callback.assertions) + if passed then + callback(true, nil) + else + callback(false, fail_message) + end + end + end) + + -- This catches errors thrown before we reach cleanup or assertions + if not success then + callback(false, err) + end +end + +---Run a synchronous test +---@param callback fun(passed: boolean, fail_message?: string) +function Test:run_sync(callback) + local success, err = pcall(self.test_body) + if success then + callback(true, nil) + else + callback(false, err) + end +end + +function Test:skip() + self.is_skipped = true + return self +end + +function Test:only() + self.is_onlyed = true + return self +end + +function Test:cleanup() + self.has_run = false + self.result = nil +end + +---@param tests Test[] +M.describe = function(description, tests) + local describe_id = u.UUID() + + local contains_onlyed = u.list_find(tests, function(test) + return test.is_onlyed + end) + + if contains_onlyed then + for _, test in ipairs(tests) do + if not test.is_onlyed then + test:skip() + end + end + end + + for _, test in ipairs(tests) do + test.describe_id = describe_id + test.describe_title = description + test_queue.add(test) + end +end + +return M diff --git a/test_framework/utils.lua b/test_framework/utils.lua new file mode 100644 index 0000000..404d953 --- /dev/null +++ b/test_framework/utils.lua @@ -0,0 +1,61 @@ +local M = {} + +function M.UUID() + local handle = io.popen 'uuidgen' + if handle then + local uuid = handle:read '*a' + handle:close() + return uuid + end + error 'uuidgen failed' +end + +---@param list table +---@param fn fun(element: any): boolean +---@return boolean +function M.list_find(list, fn) + for _, value in ipairs(list) do + if fn(value) then + return value + end + end + return false +end + +---Used when running tests headlessly +---@param status ('success' | 'failed') +function M.exit_status_code(status) + if status == 'success' then + M.print 'Exiting with status code 0' + vim.cmd '0cq' + else + M.print 'Exiting with status code 1' + vim.cmd '1cq' + end +end + +---Decides how to output messages, based on whether we're running headlessly +---@param str string +---@param level? ('error' | 'warn' | 'info' | 'success') +function M.print(str, level) + if M.is_headless() then + io.stdout:write(str) + io.stdout:write '\n' + else + local highlight = 'Normal' + if level == 'error' then + highlight = 'Error' + elseif level == 'warn' then + highlight = 'WarningMsg' + elseif level == 'success' then + highlight = 'String' + end + vim.api.nvim_echo({ { str, highlight } }, true, {}) + end +end + +function M.is_headless() + return vim.fn.getenv 'HEADLESS' == 'true' +end + +return M diff --git a/tests/TESTS_README.md b/tests/TESTS_README.md new file mode 100644 index 0000000..ebb31cf --- /dev/null +++ b/tests/TESTS_README.md @@ -0,0 +1,24 @@ +# Testing + +This project has a bespoke test framework, created to more easily handle the async requirements of testing Triptych. + +It works somewhat like [Cypress](https://www.cypress.io) in that tests performs user actions, and then assert what's displayed on screen +(or anything really, like filesystem changes) + +### Test playground + +Since the UI tests perform real user actions, this means real filesystem changes. As such the `test_playground` directory +exists as a safe place to run such actions and to simulate a real project environment. + +## Running tests + +Individual spec files can be run by sourcing them. e.g. `:source %` or `:so%`. + +There's also `run_specs.lua` which does this for all files in the `specs` directory. Just do `so%` from `run_specs.lua`. + +Tests can also be run headlessly from outside Neovim, by doing. + +``` +cd +HEADLESS=true nvim --headless +"so%" tests/run_specs.lua +``` diff --git a/tests/run_specs.lua b/tests/run_specs.lua new file mode 100644 index 0000000..8e8dac0 --- /dev/null +++ b/tests/run_specs.lua @@ -0,0 +1,49 @@ +local u = require 'test_framework.utils' +local uv = vim.loop + +local function get_files_in_dir(dir) + local files = {} + local handle = uv.fs_scandir(dir) + + if handle then + local name, type = uv.fs_scandir_next(handle) + while name do + local full_path = dir .. '/' .. name + if type == 'file' then + table.insert(files, full_path) + elseif type == 'directory' then + -- Recursively list subdirectories if needed + local sub_files = get_files_in_dir(full_path) + vim.list_extend(files, sub_files) + end + name, type = uv.fs_scandir_next(handle) + end + end + + return files +end + +local function run_specs() + vim.schedule(function() + local cwd = vim.fn.getcwd() + local spec_dir = cwd .. '/tests/specs' + local specs = get_files_in_dir(spec_dir) + for _, spec in ipairs(specs) do + vim.cmd(':source ' .. spec) + end + end) +end + +if u.is_headless() then + vim.api.nvim_create_autocmd({ 'VimEnter' }, { + callback = function() + -- The "VeryLazy" event doesn't seem to run in headless mode, so we need to call setup() manually. + -- Probably not a bad thing anyway, to use the default config for tests. + require('triptych').setup() + run_specs() + end, + }) +else + -- This allows to us to run specs by sourcing this file using :so% + run_specs() +end diff --git a/unit_tests/config_spec.lua b/tests/specs/config_spec.lua similarity index 89% rename from unit_tests/config_spec.lua rename to tests/specs/config_spec.lua index 6ab58fe..9af8091 100644 --- a/unit_tests/config_spec.lua +++ b/tests/specs/config_spec.lua @@ -1,9 +1,13 @@ +local assert = require 'luassert' +local u = require 'tests.utils' local config = require 'triptych.config' -local u = require 'triptych.utils' +local framework = require 'test_framework.test' +local it = framework.test +local describe = framework.describe ----@return TriptychConfig local function expected_default_config() return { + debug = false, mappings = { show_help = 'g?', jump_to_cwd = '.', @@ -70,14 +74,12 @@ local function expected_default_config() } end -describe('create_merged_config', function() +describe('create_merged_config', { it('returns the default config when user config is empty', function() - _G.triptych_mock_vim = {} assert.same(expected_default_config(), config.create_merged_config {}) - end) + end), it('merges partial user config with the default', function() - _G.triptych_mock_vim = {} local default_config = expected_default_config() local user_config = { mappings = { @@ -94,5 +96,5 @@ describe('create_merged_config', function() return result end) assert.same(expected, config.create_merged_config(user_config)) - end) -end) + end), +}) diff --git a/tests/specs/help_spec.lua b/tests/specs/help_spec.lua new file mode 100644 index 0000000..d499de3 --- /dev/null +++ b/tests/specs/help_spec.lua @@ -0,0 +1,32 @@ +local help = require 'triptych.help' +local assert = require 'luassert' +local framework = require 'test_framework.test' +local it = framework.test +local describe = framework.describe + +describe('help_lines', { + it('returns key bindings', function() + local result = help.help_lines() + + assert.same({ + 'Triptych key bindings', + '', + '[-] : open_hsplit', + '[.] : jump_to_cwd', + '[] : open_tab', + '[.] : toggle_hidden', + '[cd] : cd', + '[a] : add', + '[c] : copy', + '[d] : delete', + '[g?] : show_help', + '[h] : nav_left', + '[l, ] : nav_right', + '[p] : paste', + '[q] : quit', + '[r] : rename', + '[x] : cut', + '[|] : open_vsplit', + }, result) + end), +}) diff --git a/tests/specs/ui_spec.lua b/tests/specs/ui_spec.lua new file mode 100644 index 0000000..52537ca --- /dev/null +++ b/tests/specs/ui_spec.lua @@ -0,0 +1,492 @@ +local assert = require 'luassert' +local u = require 'tests.utils' +local framework = require 'test_framework.test' +local describe = framework.describe +local test = framework.test_async + +local cwd = vim.fn.getcwd() +local opening_dir = u.join_path(cwd, 'tests/test_playground/level_1/level_2/level_3') + +---@param callback function +local function open_triptych(callback) + u.open_triptych(opening_dir) + u.on_all_wins_updated(callback) +end + +---@param callback function +local function close_triptych(callback) + u.on_event('TriptychDidClose', callback) + u.press_keys 'q' +end + +describe('Triptych UI', { + test('opens on Triptych command, with cursor on current file', function(done) + u.setup_triptych() + vim.cmd 'Triptych' + u.on_all_wins_updated(function() + local is_open = vim.g.triptych_is_open + local current_line = vim.api.nvim_get_current_line() + close_triptych(function() + done { + assertions = function() + assert.same(is_open, true) + -- This is kinda janky, but basically this test could be called from one of 2 places + assert(current_line == 'run_specs.lua' or current_line == 'ui_spec.lua', 'got ' .. tostring(current_line)) + end, + } + end) + end) + end), + + test('closes on Triptych command', function(done) + open_triptych(function() + u.on_event('TriptychDidClose', function() + done { + assertions = function() + assert.same(vim.g.triptych_is_open, false) + end, + } + end) + vim.cmd 'Triptych' + end) + end), + + test('populates windows with files and folders', function(done) + local expected_lines = { + child = { 'level_5/', 'level_4_file_1.lua' }, + primary = { 'level_4/', 'level_3_file_1.md' }, + parent = { 'level_3/', 'level_2_file_1.lua' }, + } + + local expected_winbars = { + child = '%#WinBar#%=%#WinBar#level_4/%=', + primary = '%#WinBar#%=%#WinBar#level_3%=', + parent = '%#WinBar#%=%#WinBar#level_2%=', + } + + local result + + open_triptych(function() + result = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines, result.lines) + assert.same(expected_winbars, result.winbars) + end, + } + end) + end) + end), + + test('navigates down the filesystem', function(done) + local expected_lines = { + child = { 'level_5_file_1.lua' }, + primary = { 'level_5/', 'level_4_file_1.lua' }, + parent = { 'level_4/', 'level_3_file_1.md' }, + } + + local expected_winbars = { + child = '%#WinBar#%=%#WinBar#level_5/%=', + primary = '%#WinBar#%=%#WinBar#level_4%=', + parent = '%#WinBar#%=%#WinBar#level_3%=', + } + + open_triptych(function() + u.press_keys 'l' + u.on_all_wins_updated(function() + local result = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines, result.lines) + assert.same(expected_winbars, result.winbars) + end, + } + end) + end) + end) + end), + + test('navigates up the filesystem', function(done) + local expected_lines = { + child = { 'level_4/', 'level_3_file_1.md' }, + primary = { 'level_3/', 'level_2_file_1.lua' }, + parent = { 'level_2/', 'level_1_file_1.lua' }, + } + + local expected_winbars = { + child = '%#WinBar#%=%#WinBar#level_3/%=', + primary = '%#WinBar#%=%#WinBar#level_2%=', + parent = '%#WinBar#%=%#WinBar#level_1%=', + } + + open_triptych(function() + u.press_keys 'h' + u.on_all_wins_updated(function() + local result = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines, result.lines) + assert.same(expected_winbars, result.winbars) + end, + } + end) + end) + end) + end), + + test('opens a file', function(done) + -- Used to return to this buffer, after the file is opened + local current_buf = vim.api.nvim_get_current_buf() + + open_triptych(function() + u.on_event('TriptychDidClose', function() + done { + assertions = function() + assert.same(vim.g.triptych_is_open, false) + end, + cleanup = function() + vim.api.nvim_set_current_buf(current_buf) + end, + } + end) + u.press_keys 'j' + u.on_child_window_updated(function() + u.press_keys 'l' + end) + end) + end), + + test('shows a file preview', function(done) + local expected_file_preview = { + '# This is markdown', + '', + 'Just some text', + '', + } + + open_triptych(function() + u.on_child_window_updated(function() + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_file_preview, state.lines.child) + end, + } + end) + end) + u.press_keys 'j' + end) + end), + + test('creates files and folders', function(done) + local expected_lines = { + primary = { + 'a_new_dir/', + 'level_4/', + 'a_new_file.lua', + 'level_3_file_1.md', + }, + child = { + 'another_new_file.md', + }, + } + + open_triptych(function() + u.press_keys 'a' + u.press_keys 'a_new_file.lua' + u.press_keys 'a' + u.press_keys 'a_new_dir/another_new_file.md' + u.on_wins_updated({ 'primary', 'child' }, function() + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines.primary, state.lines.primary) + assert.same(expected_lines.child, state.lines.child) + end, + cleanup = function() + vim.fn.delete(u.join_path(opening_dir, 'a_new_dir'), 'rf') + vim.fn.delete(u.join_path(opening_dir, 'a_new_file.lua')) + end, + } + end) + end) + end) + end), + + test('publishes public events on file/folder creation', function(done) + local expected_events = { + ['TriptychWillCreateNode'] = { + { path = u.join_path(opening_dir, 'a_new_file.lua') }, + { path = u.join_path(opening_dir, 'a_new_dir/another_new_file.md') }, + }, + ['TriptychDidCreateNode'] = { + { path = u.join_path(opening_dir, 'a_new_file.lua') }, + { path = u.join_path(opening_dir, 'a_new_dir/another_new_file.md') }, + }, + } + + open_triptych(function() + u.press_keys 'a' + u.press_keys 'a_new_file.lua' + u.press_keys 'a' + u.press_keys 'a_new_dir/another_new_file.md' + u.on_events({ + { name = 'TriptychWillCreateNode', wait_for_n = 2 }, + { name = 'TriptychDidCreateNode', wait_for_n = 2 }, + }, function(events) + close_triptych(function() + done { + assertions = function() + assert.same(expected_events, events) + end, + cleanup = function() + vim.fn.delete(u.join_path(opening_dir, 'a_new_dir'), 'rf') + vim.fn.delete(u.join_path(opening_dir, 'a_new_file.lua')) + end, + } + end) + end) + end) + end), + + -- Having trouble with this + -- How to programatically input a selection in vim.ui.select + test('deletes files and folders', function(done) + local expected_lines = { + primary = { + 'level_4/', + 'level_3_file_1.md', + }, + child = { + 'another_new_file.md', + }, + } + + open_triptych(function() + -- Add things + u.press_keys 'a' + u.press_keys 'a_new_file.lua' + u.press_keys 'a' + u.press_keys 'a_new_dir/another_new_file.md' + -- Then remove them + u.on_wins_updated({ 'primary', 'child' }, function() + local state = u.get_state() + u.press_keys 'd1' + u.on_wins_updated({ 'primary', 'child' }, function() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines.primary, state.lines.primary) + assert.same(expected_lines.child, state.lines.child) + end, + } + end) + end) + end) + end) + end):skip(), + + -- TODO: Skipped this because there seems to be a bug with dir pasting! + test('copies file and folders', function(done) + local expected_lines = { + primary = { + 'level_4/', + 'level_3_file_1.md', + 'level_3_file_1_copy1.md', + }, + child = { + 'level_4/', + 'level_5/', + 'level_4_file_1.lua', + }, + } + open_triptych(function() + u.press_keys 'c' + u.on_primary_window_updated(function() + u.press_keys 'p' + u.on_primary_window_updated(function() + u.press_keys 'j' + u.press_keys 'c' + u.on_primary_window_updated(function() + u.press_keys 'p' + u.on_primary_window_updated(function() + -- Go back to the top, so we can the new dir we've pasted in the the child directory + u.press_keys 'gg' + u.on_child_window_updated(function() + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines.child, state.lines.child) + assert.same(expected_lines.primary, state.lines.primary) + end, + cleanup = function() + vim.fn.delete(u.join_path(opening_dir, 'level_3_file_1_copy1.md')) + vim.fn.delete(u.join_path(opening_dir, 'level_4/level_4', 'rf')) + end, + } + end) + end) + end) + end) + end) + end) + end) + end):skip(), + + -- TODO: This once the dir pasting bug has been fixed + -- test('moves files and folders', function (done) end):skip() + + -- TODO: This once the dir pasting bug has been fixed + -- test('copies files and folders', function(done) end):skip() + + test('renames files and folders', function(done) + local expected_lines = { + primary = { + 'renamed_dir/', + 'renamed_file.lua', + }, + } + + open_triptych(function() + u.press_keys 'r' + u.press_keys 'renamed_dir' + u.on_primary_window_updated(function() + u.press_keys 'j' + u.press_keys 'r' + u.press_keys 'renamed_file.lua' + u.on_primary_window_updated(function() + local state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines.primary, state.lines.primary) + end, + cleanup = function() + vim.fn.rename(u.join_path(opening_dir, 'renamed_dir'), u.join_path(opening_dir, 'level_4')) + vim.fn.rename( + u.join_path(opening_dir, 'renamed_file.lua'), + u.join_path(opening_dir, 'level_3_file_1.md') + ) + end, + } + end) + end) + end) + end) + end), + + test('publishes public events on renaming files/folders', function(done) + local expected_data = { + { from_path = u.join_path(opening_dir, 'level_4'), to_path = u.join_path(opening_dir, 'renamed_dir') }, + { + from_path = u.join_path(opening_dir, 'level_3_file_1.md'), + to_path = u.join_path(opening_dir, 'renamed_file.lua'), + }, + } + + local expected_events = { + ['TriptychWillMoveNode'] = expected_data, + ['TriptychDidMoveNode'] = expected_data, + } + + open_triptych(function() + u.press_keys 'r' + u.press_keys 'renamed_dir' + u.on_primary_window_updated(function() + u.press_keys 'j' + u.press_keys 'r' + u.press_keys 'renamed_file.lua' + end) + u.on_events({ + { name = 'TriptychWillMoveNode', wait_for_n = 2 }, + { name = 'TriptychDidMoveNode', wait_for_n = 2 }, + }, function(events) + close_triptych(function() + done { + assertions = function() + assert.same(expected_events, events) + end, + cleanup = function() + vim.fn.rename(u.join_path(opening_dir, 'renamed_dir'), u.join_path(opening_dir, 'level_4')) + vim.fn.rename(u.join_path(opening_dir, 'renamed_file.lua'), u.join_path(opening_dir, 'level_3_file_1.md')) + end, + } + end) + end) + end) + end), + + test('toggles hidden files (dot and gitignored)', function(done) + local expected_lines_without_hidden = { + primary = { + 'level_4/', + 'level_3_file_1.md', + }, + } + local expected_lines_with_hidden = { + primary = { + 'level_4/', + '.hidden_dot_file', + 'git_ignored_file', + 'level_3_file_1.md', + }, + } + + open_triptych(function() + local first_state = u.get_state() + u.press_keys '.' + u.on_primary_window_updated(function() + local second_state = u.get_state() + u.press_keys '.' + u.on_primary_window_updated(function() + local third_state = u.get_state() + close_triptych(function() + done { + assertions = function() + assert.same(expected_lines_without_hidden.primary, first_state.lines.primary) + assert.same(expected_lines_with_hidden.primary, second_state.lines.primary) + assert.same(expected_lines_without_hidden.primary, third_state.lines.primary) + end, + } + end) + end) + end) + end) + end), + + test('jumps to cwd and back', function(done) + -- Using the winbar as a proxy for directory + local expected_winbar_after_first_jump = '%#WinBar#%=%#WinBar#triptych %#Comment#(cwd)%=' + local expected_winbar_after_second_jump = '%#WinBar#%=%#WinBar#level_3%=' + + local winbar_after_first_jump + local winbar_after_second_jump + + open_triptych(function() + u.press_keys '.' + u.on_all_wins_updated(function() + local state = u.get_state() + winbar_after_first_jump = state.winbars.primary + u.press_keys '.' + u.on_all_wins_updated(function() + local state_2 = u.get_state() + winbar_after_second_jump = state_2.winbars.primary + close_triptych(function() + done { + assertions = function() + assert.same(expected_winbar_after_first_jump, winbar_after_first_jump) + assert.same(expected_winbar_after_second_jump, winbar_after_second_jump) + end, + } + end) + end) + end) + end) + end), +}) diff --git a/unit_tests/utils_spec.lua b/tests/specs/utils_spec.lua similarity index 84% rename from unit_tests/utils_spec.lua rename to tests/specs/utils_spec.lua index aa199c5..105f978 100644 --- a/unit_tests/utils_spec.lua +++ b/tests/specs/utils_spec.lua @@ -1,17 +1,21 @@ +local assert = require 'luassert' local u = require 'triptych.utils' +local framework = require 'test_framework.test' +local it = framework.test +local describe = framework.describe -describe('set', function() +describe('set', { it('returns a copy of the table with the specified value changed', function() local tbl = { foo = 1, bar = 2, } local result = u.set(tbl, 'foo', 3) - assert.are.same(3, result.foo) - end) -end) + assert.same(3, result.foo) + end), +}) -describe('merge_tables', function() +describe('merge_tables', { it('merges tables - none empty', function() local a = { foo = 'bar', @@ -34,7 +38,7 @@ describe('merge_tables', function() } local result = u.merge_tables(a, b) assert.same(expected, result) - end) + end), it('merges tables - first one is empty', function() local a = {} @@ -50,7 +54,7 @@ describe('merge_tables', function() } local result = u.merge_tables(a, b) assert.same(expected, result) - end) + end), it('merges tables - second one empty', function() local a = { @@ -68,10 +72,10 @@ describe('merge_tables', function() } local result = u.merge_tables(a, b) assert.same(expected, result) - end) -end) + end), +}) -describe('round', function() +describe('round', { it('rounds to x decimal places', function() assert.same(0.33, u.round(0.333, 2)) assert.same(0.333, u.round(0.333, 3)) @@ -79,5 +83,5 @@ describe('round', function() assert.same(1.1, u.round(1.11, 1)) assert.same(1, u.round(1.16, 0)) assert.same(200.99, u.round(200.99, 3)) - end) -end) + end), +}) diff --git a/tests/test_playground/level_1/level_1_file_1.lua b/tests/test_playground/level_1/level_1_file_1.lua new file mode 100644 index 0000000..776b4d0 --- /dev/null +++ b/tests/test_playground/level_1/level_1_file_1.lua @@ -0,0 +1 @@ +-- Nothing to see here diff --git a/tests/test_playground/level_1/level_2/level_2_file_1.lua b/tests/test_playground/level_1/level_2/level_2_file_1.lua new file mode 100644 index 0000000..f49d6dc --- /dev/null +++ b/tests/test_playground/level_1/level_2/level_2_file_1.lua @@ -0,0 +1 @@ +return 'foo' diff --git a/tests/test_playground/level_1/level_2/level_3/.hidden_dot_file b/tests/test_playground/level_1/level_2/level_3/.hidden_dot_file new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_playground/level_1/level_2/level_3/level_3_file_1.md b/tests/test_playground/level_1/level_2/level_3/level_3_file_1.md new file mode 100644 index 0000000..e38e8b5 --- /dev/null +++ b/tests/test_playground/level_1/level_2/level_3/level_3_file_1.md @@ -0,0 +1,3 @@ +# This is markdown + +Just some text diff --git a/tests/test_playground/level_1/level_2/level_3/level_4/level_4_file_1.lua b/tests/test_playground/level_1/level_2/level_3/level_4/level_4_file_1.lua new file mode 100644 index 0000000..cbcf620 --- /dev/null +++ b/tests/test_playground/level_1/level_2/level_3/level_4/level_4_file_1.lua @@ -0,0 +1,5 @@ +local function say_hello() + vim.print 'hello' +end + +return say_hello diff --git a/tests/test_playground/level_1/level_2/level_3/level_4/level_5/level_5_file_1.lua b/tests/test_playground/level_1/level_2/level_3/level_4/level_5/level_5_file_1.lua new file mode 100644 index 0000000..a2e8eae --- /dev/null +++ b/tests/test_playground/level_1/level_2/level_3/level_4/level_5/level_5_file_1.lua @@ -0,0 +1,3 @@ +local foo = 'bar' + +return foo diff --git a/tests/utils.lua b/tests/utils.lua new file mode 100644 index 0000000..eb05f89 --- /dev/null +++ b/tests/utils.lua @@ -0,0 +1,218 @@ +local api = vim.api + +local M = {} + +---@param ... string +---@return string +function M.join_path(...) + return table.concat({ ... }, '/') +end + +function M.get_lines(buf) + return vim.api.nvim_buf_get_lines(buf, 0, -1, false) +end + +function M.get_winbar(win) + return api.nvim_get_option_value('winbar', { win = win }) +end + +---@param event string +---@param callback function +---@param once? boolean if false, remember to cleanup with nvim_del_autocmd +function M.on_event(event, callback, once) + if once == nil then + once = true + end + vim.api.nvim_create_autocmd('User', { + group = 'TriptychEvents', + pattern = event, + once = once, + callback = vim.schedule_wrap(function(data) + callback(data) + return true + end), + }) +end + +---@param events { name: string, wait_for_n: integer }[] +---@param callback fun(result: table) +function M.on_events(events, callback) + ---@type table + local result = {} + + local autocmd_ids = {} + + local function is_ready() + for _, event in ipairs(events) do + local entry = result[event.name] or {} + if #entry < event.wait_for_n then + return false + end + end + return true + end + + for _, event in ipairs(events) do + result[event.name] = {} + + local timer = vim.loop.new_timer() + + local id = M.on_event(event.name, function(data) + timer:stop() + + table.insert(result[event.name], data.data) + + if is_ready() then + timer:start( + 1000, + 0, + vim.schedule_wrap(function() + for _, id in ipairs(autocmd_ids) do + api.nvim_del_autocmd(id) + end + callback(result) + end) + ) + end + end, false) + + table.insert(autocmd_ids, id) + end +end + +function M.on_child_window_updated(callback) + M.on_wins_updated({ 'child' }, callback) +end + +function M.on_primary_window_updated(callback) + M.on_wins_updated({ 'primary' }, callback) +end + +function M.on_all_wins_updated(callback) + M.on_wins_updated({ 'child', 'primary', 'parent' }, callback) +end + +---@param wins ('child' | 'primary'| 'parent')[] +---@param callback function +function M.on_wins_updated(wins, callback) + local wait_for_child = M.list_contains(wins, 'child') + local wait_for_primary = M.list_contains(wins, 'primary') + local wait_for_parent = M.list_contains(wins, 'parent') + + -- We're essentially saying, if the wins list does't contain X, then we consider it already updated + local wins_updated = { + child = not wait_for_child, + primary = not wait_for_primary, + parent = not wait_for_parent, + } + -- Timer is used to wait 1 second before executing the callback + -- Just to make sure there are no more events coming through + local timer = vim.loop.new_timer() + M.on_event('TriptychDidUpdateWindow', function(data) + timer:stop() + wins_updated[data.data.win_type] = true + if wins_updated.child and wins_updated.primary and wins_updated.parent then + timer:start( + 1000, + 0, + vim.schedule_wrap(function() + api.nvim_del_autocmd(data.id) + callback() + end) + ) + end + end, false) +end + +function M.setup_triptych() + require('triptych.init').setup { + debug = false, + -- Set options for easier testing + options = { + file_icons = { + enabled = false, + }, + syntax_highlighting = { + enabled = false, + }, + }, + } +end + +function M.open_triptych(opening_dir) + M.setup_triptych() + require('triptych.init').toggle_triptych(opening_dir) +end + +function M.get_state() + local all_windows = api.nvim_list_wins() + local wins = { + child = -1, + primary = -1, + parent = -1, + } + for _, win in ipairs(all_windows) do + local has_role, role = pcall(api.nvim_win_get_var, win, 'triptych_role') + if has_role then + wins[role] = win + end + end + + local bufs = { + child = api.nvim_win_get_buf(wins.child), + primary = api.nvim_win_get_buf(wins.primary), + parent = api.nvim_win_get_buf(wins.parent), + } + + local lines = { + child = M.get_lines(bufs.child), + primary = M.get_lines(bufs.primary), + parent = M.get_lines(bufs.parent), + } + + local winbars = { + child = M.get_winbar(wins.child), + primary = M.get_winbar(wins.primary), + parent = M.get_winbar(wins.parent), + } + + return { + wins = wins, + bufs = bufs, + lines = lines, + winbars = winbars, + } +end + +---@param key string +function M.press_keys(key) + local input_parsed = api.nvim_replace_termcodes(key, true, true, true) + api.nvim_feedkeys(input_parsed, 'normal', false) +end + +function M.reverse_list(list) + local reversed = {} + for i = #list, 1, -1 do + table.insert(reversed, list[i]) + end + return reversed +end + +---@param list string[] +---@param str string +---@return boolean +function M.list_contains(list, str) + for _, value in ipairs(list) do + if value == str then + return true + end + end + return false +end + +---@param fn function +function M.eval(fn) + return fn() +end + +return M diff --git a/ui_tests/setup.lua b/ui_tests/setup.lua deleted file mode 100644 index 78c0059..0000000 --- a/ui_tests/setup.lua +++ /dev/null @@ -1,94 +0,0 @@ -local u = require 'triptych.utils' - -local M = {} - -local this_dir = vim.fs.dirname(vim.api.nvim_buf_get_name(0)) -local test_playground_dirname = 'test_playground' -M.test_playground_path = this_dir .. '/' .. test_playground_dirname - -local function escape_double_quotes(str) - return string.gsub(str, '"', '\\"') -end - -local function create_file_or_dir(dir_path, file_or_dir) - if file_or_dir.dir then - local dir = file_or_dir - local new_dir_path = dir_path .. '/' .. dir.dir - vim.fn.system('mkdir ' .. new_dir_path) - if u.is_defined(dir.children) then - for i = 1, #dir.children, 1 do - create_file_or_dir(new_dir_path, dir.children[i]) - end - end - else - local file = file_or_dir - local file_path = dir_path .. '/' .. file.file - vim.fn.system('touch ' .. file_path) - if u.is_defined(file.lines) then - vim.fn.system('echo "' .. escape_double_quotes(file.lines) .. '" > ' .. file_path) - end - end -end - -function M.cleanup() - vim.fn.system('rm -rf ' .. M.test_playground_path) -end - -M.js_lines = [[ -const hello = "world" -console.log(1 + 1) -]] - -M.java_lines = [[ -class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello, World!"); - } -} -]] - -M.lua_lines = [[ -local greeting = "hello world" -vim.print(greeting) -]] - -function M.setup() - local files_and_dirs = { - dir = test_playground_dirname, - children = { - { - dir = 'level_1_dir_1', - children = { - { - dir = 'level_2_dir_1', - children = { - { - dir = 'level_3_dir_1', - children = { - { - dir = 'level_4_dir_1', - children = { - { file = 'level_5_file_1.lua', lines = M.lua_lines }, - }, - }, - { file = 'level_4_file_1.js', lines = M.js_lines }, - }, - }, - { file = 'level_3_file_1.java', lines = M.java_lines }, - }, - }, - { file = 'level_2_file_1.java', lines = M.java_lines }, - { file = 'level_2_file_2.sh' }, - { file = 'level_2_file_3.php' }, - }, - }, - { file = 'level_1_file_1.js', lines = M.js_lines }, - { file = 'level_1_file_2.ts' }, - { file = 'level_1_file_3.lua' }, - { dir = 'level_1_dir_2', children = {} }, - }, - } - create_file_or_dir(this_dir, files_and_dirs) -end - -return M diff --git a/ui_tests/tests.lua b/ui_tests/tests.lua deleted file mode 100644 index b1676e4..0000000 --- a/ui_tests/tests.lua +++ /dev/null @@ -1,212 +0,0 @@ -local test_setup = require 'ui_tests.setup' -local tu = require 'ui_tests.utils' -local tryptic = require 'triptych.init' -local u = require 'triptych.utils' - --- TODO: I've removed these tests from validate.sh and ci.yml --- They are no longer working since autocmds were made buffer local --- I need to find a more robust way of testing - ----@return TriptychConfig -local function test_config() - return { - options = { - syntax_highlighting = { - enabled = false, - debounce_ms = 0, - }, - file_icons = { - enabled = false, - directory_icon = '+', - fallback_file_icon = '-', - }, - }, - } -end - ----@param config? table ----@return fun() -local function open_triptych(config) - tryptic.setup(config or test_config()) - local close_fn = tryptic.toggle_triptych(test_setup.test_playground_path .. '/level_1_dir_1/level_2_dir_1') - tu.wait() - ---@diagnostic disable-next-line: return-type-mismatch - return close_fn -end - ---- Opens tripytic with the config defined above, runs the test case, then closes tripytic ----@param fn function - test ----@return nil -local function with_default_config_and_close(fn) - local close = open_triptych() - fn() - close() -end - -describe('triptych', function() - before_each(function() - test_setup.cleanup() - test_setup.setup() - end) - - after_each(function() - test_setup.cleanup() - end) - - it('closes when user inputs the configured key (default q).', function() - local close = open_triptych() - tu.user_input 'q' - local success, _ = pcall(close) - assert(success == false, 'Expected close to fail because triptic should already be closed') - end) - - it('closes when user calls the Triptych() command and triptych is already open', function() - local close = open_triptych() - vim.cmd.Triptych() - local success, _ = pcall(close) - assert(success == false, 'Expected close to fail because triptic should already be closed') - end) - - it('populates the parent, primary and child windows when launched', function() - with_default_config_and_close(function() - assert.same({ - 'level_2_dir_1/', - 'level_2_file_1.java', - 'level_2_file_2.sh', - 'level_2_file_3.php', - }, tu.get_lines 'parent') - assert.same({ - 'level_3_dir_1/', - 'level_3_file_1.java', - }, tu.get_lines 'primary') - assert.same({ - 'level_4_dir_1/', - 'level_4_file_1.js', - }, tu.get_lines 'child') - end) - end) - - it('updates the child window when the cursor moves onto a file', function() - with_default_config_and_close(function() - tu.move 'down' - local expected = u.multiline_str_to_table(test_setup.java_lines) - table.insert(expected, '') -- TODO: Why - assert.same(expected, tu.get_lines 'child') - end) - end) - - it('updates all windows when navigating to the parent directory', function() - with_default_config_and_close(function() - tu.move 'left' - assert.same({ - 'level_1_dir_1/', - 'level_1_dir_2/', - 'level_1_file_1.js', - 'level_1_file_2.ts', - 'level_1_file_3.lua', - }, tu.get_lines 'parent') - assert.same({ - 'level_2_dir_1/', - 'level_2_file_1.java', - 'level_2_file_2.sh', - 'level_2_file_3.php', - }, tu.get_lines 'primary') - assert.same({ - 'level_3_dir_1/', - 'level_3_file_1.java', - }, tu.get_lines 'child') - end) - end) - - it('updates all windows when navigating to a child directory', function() - with_default_config_and_close(function() - tu.move 'right' - assert.same({ - 'level_5_file_1.lua', - }, tu.get_lines 'child') - end) - end) - - it('shows key binding when user inputs the configured help binding (default g?)', function() - with_default_config_and_close(function() - tu.user_input 'g?' - assert.same('Triptych key bindings', tu.get_lines('child')[1]) - end) - end) - - it('passes the expected values to an extension mapping function', function() - local result - local close = open_triptych { - extension_mappings = { - ['+'] = { - mode = 'n', - fn = function(arg) - result = arg - end, - }, - }, - } - tu.user_input '+' - assert.same({ - children = {}, - dirname = './test_playground/level_1_dir_1/level_2_dir_1', - display_name = 'level_3_dir_1/', - is_dir = true, - path = './test_playground/level_1_dir_1/level_2_dir_1/level_3_dir_1', - }, result) - close() - end) - - -- it('copies a file', function() - -- with_default_config_and_close(function() - -- tu.move 'down' - -- tu.user_input 'c' - -- assert.same({ - -- 'level_3_dir_1/', - -- 'level_3_file_1.java (copy)', - -- }, tu.get_lines 'primary') - -- tu.move 'left' - -- tu.move 'down' - -- tu.user_input 'p' - -- assert.same({ - -- 'level_2_dir_1/', - -- 'level_2_file_1.java', - -- 'level_2_file_2.sh', - -- 'level_2_file_3.php', - -- 'level_3_file_1.java', - -- }, tu.get_lines 'primary') - -- end) - -- end) - - it('deletes a file', function() - with_default_config_and_close(function() - tu.move 'down' - tu.user_input 'd1' - assert.same({ - 'level_3_dir_1/', - }, tu.get_lines 'primary') - end) - end) - - it('deletes a directory', function() - with_default_config_and_close(function() - tu.user_input 'd1' - assert.same({ - 'level_3_file_1.java', - }, tu.get_lines 'primary') - end) - end) - - it('deletes a selection', function() - with_default_config_and_close(function() - tu.move 'left' - tu.user_input 'V' - tu.move 'down' - tu.user_input 'd1' - assert.same({ - 'level_2_file_2.sh', - 'level_2_file_3.php', - }, tu.get_lines 'primary') - end) - end) -end) diff --git a/ui_tests/utils.lua b/ui_tests/utils.lua deleted file mode 100644 index f3f108d..0000000 --- a/ui_tests/utils.lua +++ /dev/null @@ -1,57 +0,0 @@ -local u = require 'triptych.utils' - -local M = {} - ----@param role 'parent' | 'primary' | 'child' ----@return table -function M.get_lines(role) - local wins = vim.api.nvim_list_wins() - for _, win in ipairs(wins) do - local _, maybe_value = pcall(vim.api.nvim_win_get_var, win, 'triptych_role') - if maybe_value == role then - local buf = vim.api.nvim_win_get_buf(win) - return vim.api.nvim_buf_get_lines(buf, 0, -1, false) - end - end - return {} -end - ----@return nil -function M.wait() - local co = coroutine.running() - vim.defer_fn(function() - coroutine.resume(co) - end, 50) - coroutine.yield() -end - ----@param inputs string|string[] ----@return nil -function M.user_input(inputs) - local input_list = u.cond(type(inputs) == 'table', { - when_true = inputs, - when_false = { inputs }, - }) - for _, input in ipairs(input_list) do - local input_parsed = vim.api.nvim_replace_termcodes(input, true, true, true) - vim.api.nvim_feedkeys(input_parsed, 'normal', false) - M.wait() - end -end - ----@param direction 'left' | 'right' | 'up' | 'down' ----@return nil -function M.move(direction) - if direction == 'left' then - M.user_input 'h' - elseif direction == 'right' then - M.user_input 'l' - elseif direction == 'down' then - M.user_input 'j' - elseif direction == 'up' then - M.user_input 'k' - end - M.wait() -end - -return M diff --git a/unit_tests/autocmds_spec.lua b/unit_tests/autocmds_spec.lua deleted file mode 100644 index 2b76ff1..0000000 --- a/unit_tests/autocmds_spec.lua +++ /dev/null @@ -1,39 +0,0 @@ -local autocmds = require 'triptych.autocmds' - -local mock_state = { - windows = { - current = { - win = 4, - }, - }, -} -local mock_diagnostic = { 'mock_diagnostic' } -local mock_git = { 'mock_git' } - -describe('AutoCommands:destroy_autocommands', function() - it('destroys the autocommands', function() - local spy = {} - local i = 0 - _G.triptych_mock_vim = { - api = { - nvim_create_autocmd = function(_, _) - i = i + 1 - return i - end, - nvim_del_autocmd = function(id) - table.insert(spy, id) - end, - nvim_win_get_buf = function(_) - return 4 - end, - }, - } - local event_handlers = { - handle_cursor_moved = function(_, _, _) end, - handle_buf_leave = function() end, - } - local AutoCmds = autocmds.new(event_handlers, mock_state, mock_diagnostic, mock_git) - AutoCmds:destroy_autocommands() - assert.same({ 1, 2, 3, 4 }, spy) - end) -end) diff --git a/unit_tests/diagnostics_spec.lua b/unit_tests/diagnostics_spec.lua deleted file mode 100644 index a734442..0000000 --- a/unit_tests/diagnostics_spec.lua +++ /dev/null @@ -1,58 +0,0 @@ -local diagnostics = require 'triptych.diagnostics' -local tu = require 'unit_tests.test_utils' - -describe('get_sign', function() - it('returns the sign name for a diagnostic severity', function() - local err = diagnostics.get_sign(1) - local warn = diagnostics.get_sign(2) - local info = diagnostics.get_sign(3) - local hint = diagnostics.get_sign(4) - assert.equal(err, 'DiagnosticSignError') - assert.equal(warn, 'DiagnosticSignWarn') - assert.equal(info, 'DiagnosticSignInfo') - assert.equal(hint, 'DiagnosticSignHint') - end) -end) - -describe('Diagnostics', function() - it('exposes diagnostics per path', function() - _G.triptych_mock_vim = { - diagnostic = { - get = function() - return { - { bufnr = 12, severity = 1 }, - { bufnr = 14, severity = 3 }, - { bufnr = 4, severity = 4 }, - } - end, - }, - api = { - nvim_buf_get_name = function(bufnr) - if bufnr == 12 then - return '/a/b/foo.js' - end - if bufnr == 14 then - return '/a/b/bar.js' - end - return '/a/b/baz.js' - end, - }, - fs = { - parents = tu.iterator { '/a/b/', '/a/', '/' }, - }, - fn = { - getcwd = function() - return '' - end, - }, - } - local Diagnostics = diagnostics.new() - assert.same(1, Diagnostics:get '/a/b/foo.js') - assert.same(3, Diagnostics:get '/a/b/bar.js') - assert.same(4, Diagnostics:get '/a/b/baz.js') - assert.same(1, Diagnostics:get '/a/b/') - assert.same(1, Diagnostics:get '/a/') - assert.same(1, Diagnostics:get '/') - assert.same(nil, Diagnostics:get '/should/not/throw.js') - end) -end) diff --git a/unit_tests/event_handlers_spec.lua b/unit_tests/event_handlers_spec.lua deleted file mode 100644 index c34d222..0000000 --- a/unit_tests/event_handlers_spec.lua +++ /dev/null @@ -1,47 +0,0 @@ -local event_handlers = require 'triptych.event_handlers' - -describe('handle_cursor_moved', function() - it('makes the expected function calls and updates path_to_line_map', function() - -- spys - local get_target_under_cursor_spy = {} - local set_child_window_target_spy = {} - local nvim_win_get_cursor_spy = {} - - -- mocks - local mock_target = { 'mock_target' } - local mock_state = { - windows = { - current = { - path = 'a/b/c', - }, - }, - path_to_line_map = { - ['a/b/c'] = 2, - }, - } - _G.triptych_mock_vim = { - api = { - nvim_win_get_cursor = function(winid) - table.insert(nvim_win_get_cursor_spy, winid) - return { 13 } - end, - }, - } - _G.triptych_mock_view = { - get_target_under_cursor = function(s) - table.insert(get_target_under_cursor_spy, s) - return mock_target - end, - set_child_window_target = function(s, f) - table.insert(set_child_window_target_spy, { s, f }) - end, - } - - event_handlers.handle_cursor_moved(mock_state) - - assert.same({ 0 }, nvim_win_get_cursor_spy) - assert.same({ { mock_state, mock_target } }, set_child_window_target_spy) - assert.same({ mock_state }, get_target_under_cursor_spy) - assert.same(13, mock_state.path_to_line_map['a/b/c']) - end) -end) diff --git a/unit_tests/fs_spec.lua b/unit_tests/fs_spec.lua deleted file mode 100644 index 686ef38..0000000 --- a/unit_tests/fs_spec.lua +++ /dev/null @@ -1,31 +0,0 @@ -local fs = require 'triptych.fs' -local plenary_filetype = require 'plenary.filetype' - -describe('get_filetype_from_path', function() - it('uses plenary detect', function() - local spy = {} - plenary_filetype.detect = function(path) - table.insert(spy, path) - return '' - end - fs.get_filetype_from_path './hello' - assert.same({ './hello' }, spy) - end) -end) - -describe('get_file_size_in_kb', function() - it('returns vim.fn.getfsize / 1000', function() - local spy = {} - _G.triptych_mock_vim = { - fn = { - getfsize = function(path) - table.insert(spy, path) - return 2000 - end, - }, - } - local result = fs.get_file_size_in_kb 'hello' - assert.same({ 'hello' }, spy) - assert.same(2, result) - end) -end) diff --git a/unit_tests/git_spec.lua b/unit_tests/git_spec.lua deleted file mode 100644 index 20e1606..0000000 --- a/unit_tests/git_spec.lua +++ /dev/null @@ -1,96 +0,0 @@ -local git = require 'triptych.git' -local tu = require 'unit_tests.test_utils' -local config = require 'triptych.config' - -local mocks = { - git_status = 'M lua/foo.lua\nA lua/bar.lua\nR docs/README.md\n?? .DS_Store', - project_root = '/hello/world', -} - -describe('Git.new', function() - it('sets project_root and status', function() - local spies = { - system = {}, - sign_getdefined = {}, - sign_define = {}, - } - _G.triptych_mock_vim = { - g = { - triptych_config = config.create_merged_config {}, - }, - fn = { - getcwd = function() - return mocks.project_root - end, - system = function(cmd) - table.insert(spies.system, cmd) - if cmd == 'git status --porcelain' then - return mocks.git_status - elseif cmd == 'git rev-parse --show-toplevel' then - return mocks.project_root - end - return nil - end, - sign_getdefined = function(name) - table.insert(spies.sign_getdefined, name) - return {} - end, - sign_define = function(name, conf) - table.insert(spies.sign_define, { name, conf.text, conf.texthl }) - end, - }, - fs = { - parents = tu.iterator {}, - }, - } - local Git = git.Git.new() - assert.same({ - 'git status --porcelain', - 'git rev-parse --show-toplevel', - }, spies.system) - assert.same(mocks.project_root, Git.project_root) - assert.same({ - ['/hello/world/lua/foo.lua'] = 'M', - ['/hello/world/lua/bar.lua'] = 'A', - ['/hello/world/docs/README.md'] = 'R', - ['/hello/world/.DS_Store'] = '??', - }, Git.status) - table.sort(spies.sign_getdefined) - assert.same( - { 'TriptychGitAdd', 'TriptychGitModify', 'TriptychGitRename', 'TriptychGitUntracked' }, - spies.sign_getdefined - ) - end) -end) - -describe('Git:status_of', function() - it('returns the git status of a path', function() - _G.triptych_mock_vim = { - g = { - triptych_config = config.create_merged_config {}, - }, - fn = { - getcwd = function() - return mocks.project_root - end, - system = function(cmd) - if cmd == 'git status --porcelain' then - return mocks.git_status - elseif cmd == 'git rev-parse --show-toplevel' then - return mocks.project_root - end - return nil - end, - sign_getdefined = function(_) - return { name = 'foo', text = '+', texthl = 'Error' } - end, - }, - fs = { - parents = tu.iterator {}, - }, - } - local Git = git.Git.new() - assert('AM', Git:status_of '/hello/world/lua/bar.lua') - assert('??', Git:status_of '/hello/world/docs/README.md') - end) -end) diff --git a/unit_tests/help_spec.lua b/unit_tests/help_spec.lua deleted file mode 100644 index 4be204f..0000000 --- a/unit_tests/help_spec.lua +++ /dev/null @@ -1,47 +0,0 @@ -local help = require 'triptych.help' - -describe('help_lines', function() - it('returns key bindings', function() - local mappings = { - show_help = 'g?', - jump_to_cwd = '.', - nav_left = '<', - nav_right = '>', - delete = 'd', - add = { 'a', 'A' }, - copy = 'c', - rename = 'r', - cut = 'x', - paste = 'p', - quit = 'q', - toggle_hidden = ',', - } - - _G.triptych_mock_vim = { - g = { - triptych_config = { - mappings = mappings, - }, - }, - } - - local result = help.help_lines() - - assert.same({ - 'Triptych key bindings', - '', - '[.] : jump_to_cwd', - '[<] : nav_left', - '[,] : toggle_hidden', - '[>] : nav_right', - '[a, A] : add', - '[c] : copy', - '[d] : delete', - '[g?] : show_help', - '[p] : paste', - '[q] : quit', - '[r] : rename', - '[x] : cut', - }, result) - end) -end) diff --git a/unit_tests/init_spec.lua b/unit_tests/init_spec.lua deleted file mode 100644 index 0dc9e53..0000000 --- a/unit_tests/init_spec.lua +++ /dev/null @@ -1,327 +0,0 @@ -local init = require 'triptych.init' -local config = require 'triptych.config' -local float = require 'triptych.float' -local autocmds = require 'triptych.autocmds' -local state = require 'triptych.state' -local mappings = require 'triptych.mappings' -local actions = require 'triptych.actions' -local view = require 'triptych.view' -local git = require 'triptych.git' -local diagnostics = require 'triptych.diagnostics' -local event_handlers = require 'triptych.event_handlers' - -describe('setup', function() - it('creates config and Triptych command', function() - local spies = { - fn = { - has = {}, - }, - api = { - nvim_create_user_command = {}, - }, - } - _G.triptych_mock_vim = { - g = {}, - fn = { - has = function(str) - table.insert(spies.fn.has, str) - return 1 - end, - }, - api = { - nvim_create_user_command = function(name, fn, conf) - table.insert(spies.api.nvim_create_user_command, { name, fn, conf }) - end, - }, - } - init.setup {} - local expected_config = config.create_merged_config {} - assert.same({ 'nvim-0.9.0' }, spies.fn.has) - assert.same(expected_config, _G.triptych_mock_vim.g.triptych_config) - assert.same(1, #spies.api.nvim_create_user_command) - local create_cmd_args = spies.api.nvim_create_user_command[1] - assert.same('Triptych', create_cmd_args[1]) - assert.same('function', type(create_cmd_args[2])) - assert.same({}, create_cmd_args[3]) - assert.same(false, _G.triptych_mock_vim.g.triptych_is_open) - end) -end) - -describe('toggle_triptych', function() - it('opens triptych, making the expected calls', function() - local spies = { - state = { - new = {}, - }, - git = { - new = 0, - }, - diagnostics = { - new = 0, - }, - float = { - create_three_floating_windows = 0, - }, - autocmds = { - new = {}, - }, - actions = { - new = {}, - }, - mappings = { - new = {}, - }, - view = { - refresh_view = {}, - set_primary_and_parent_window_targets = {}, - }, - vim = { - api = { - nvim_buf_get_name = {}, - nvim_get_current_win = 0, - nvim_set_current_win = {}, - nvim_buf_get_option = {}, - }, - fs = { - dirname = {}, - }, - fn = { - getcwd = 0, - filereadable = {}, - }, - }, - } - - _G.triptych_mock_vim = { - o = { - columns = 160, - }, - g = { - triptych_config = config.create_merged_config {}, - }, - api = { - nvim_buf_get_name = function(bufid) - table.insert(spies.vim.api.nvim_buf_get_name, bufid) - return '/hello/world' - end, - nvim_get_current_win = function() - spies.vim.api.nvim_get_current_win = spies.vim.api.nvim_get_current_win + 1 - return 66 - end, - nvim_set_current_win = function(winid) - table.insert(spies.vim.api.nvim_set_current_win, winid) - end, - nvim_buf_get_option = function(bufid, option_name) - table.insert(spies.vim.api.nvim_buf_get_option, { bufid, option_name }) - end, - }, - fs = { - dirname = function(path) - table.insert(spies.vim.fs.dirname, path) - return vim.fs.dirname(path) - end, - }, - fn = { - filereadable = function(path) - table.insert(spies.vim.fn.filereadable, path) - return 1 - end, - }, - } - - local mock_state = { - windows = {}, - } - - state.new = function(conf, opening_winid) - table.insert(spies.state.new, { conf, opening_winid }) - return mock_state - end - - git.Git.new = function() - spies.git.new = spies.git.new + 1 - return 'mock_git' - end - - diagnostics.new = function() - spies.diagnostics.new = spies.diagnostics.new + 1 - return 'mock_diagnostic' - end - - autocmds.new = function(h, f, s, d, g) - table.insert(spies.autocmds.new, { h, f, s, d, g }) - return 'mock_autocmds' - end - - actions.new = function(_state, refresh_fn) - table.insert(spies.actions.new, { _state, refresh_fn }) - return 'mock_actions' - end - - view.refresh_view = function(s, d, g) - table.insert(spies.view.refresh_view, { s, d, g }) - end - - view.set_primary_and_parent_window_targets = function(s, o) - table.insert(spies.view.set_primary_and_parent_window_targets, { s, o }) - end - - mappings.new = function(s, a) - table.insert(spies.mappings.new, { s, a }) - end - - float.create_three_floating_windows = function() - spies.float.create_three_floating_windows = spies.float.create_three_floating_windows + 1 - return { 4, 5, 6 } - end - - init.toggle_triptych() - - assert.same({ - { _G.triptych_mock_vim.g.triptych_config, 66 }, - }, spies.state.new) - assert.same(1, spies.git.new) - assert.same(1, spies.diagnostics.new) - assert.same({ { 0, 'buftype' } }, spies.vim.api.nvim_buf_get_option) - assert.same({ 0 }, spies.vim.api.nvim_buf_get_name) - assert.same({ '/hello/world' }, spies.vim.fn.filereadable) - assert.same({ '/hello/world' }, spies.vim.fs.dirname) - assert.same(1, spies.float.create_three_floating_windows) - assert.same({ - windows = { - parent = { - path = '', - win = 4, - }, - current = { - path = '', - previous_path = '', - win = 5, - }, - child = { - is_dir = false, - win = 6, - }, - }, - }, mock_state) - assert.same({ { event_handlers, mock_state, 'mock_diagnostic', 'mock_git' } }, spies.autocmds.new) - assert.same({ { mock_state, '/hello' } }, spies.view.set_primary_and_parent_window_targets) - assert.same(mock_state, spies.actions.new[1][1]) - assert.same({ { mock_state, 'mock_actions' } }, spies.mappings.new) - end) - - it('create a close function', function() - local spies = { - autocmd_destroy = 0, - close_floats = {}, - nvim_set_current_win = {}, - view = { - set_primary_and_parent_window_targets = {}, - }, - } - - _G.triptych_mock_vim = { - o = { - columns = 160, - }, - g = { - triptych_config = config.create_merged_config {}, - }, - api = { - nvim_buf_get_name = function(_) - return '/hello/world' - end, - nvim_get_current_win = function() - return 66 - end, - nvim_set_current_win = function(winid) - table.insert(spies.nvim_set_current_win, winid) - end, - nvim_buf_get_option = function(_, _) end, - }, - fs = { - dirname = function(path) - return vim.fs.dirname(path) - end, - }, - fn = { - getcwd = function() - return '/hello' - end, - filereadable = function() - return 1 - end, - }, - } - - local mock_state = { - opening_win = 9, - windows = {}, - } - - state.new = function(_, _) - return mock_state - end - - git.Git.new = function() - return 'mock_git' - end - - diagnostics.new = function() - return 'mock_diagnostic' - end - - autocmds.new = function(_, _, _, _) - return { - destroy_autocommands = function(_) - spies.autocmd_destroy = spies.autocmd_destroy + 1 - end, - } - end - - actions.new = function(_, _) - return 'mock_actions' - end - - view.refresh_view = function(_, _, _) end - - view.set_primary_and_parent_window_targets = function(_state, opening_dir) - table.insert(spies.view.set_primary_and_parent_window_targets, { _state, opening_dir }) - end - - mappings.new = function(_, _) end - - float.create_three_floating_windows = function() - return { 4, 5, 6 } - end - - float.close_floats = function(winids) - table.insert(spies.close_floats, winids) - end - - init.toggle_triptych() - - _G.triptych_mock_vim.g.triptych_close() - - assert.same({ { mock_state, '/hello' } }, spies.view.set_primary_and_parent_window_targets) - assert.same(1, spies.autocmd_destroy) - assert.same({ { 4, 5, 6 } }, spies.close_floats) - assert.same({ 9 }, spies.nvim_set_current_win) - end) - - it("closes triptych if it's currently open", function() - local close_spy = 0 - _G.triptych_mock_vim = { - g = { - triptych_is_open = true, - triptych_close = function() - close_spy = close_spy + 1 - end, - }, - } - - init.toggle_triptych() - - assert.same(1, close_spy) - end) -end) diff --git a/unit_tests/mappings_spec.lua b/unit_tests/mappings_spec.lua deleted file mode 100644 index 649c67e..0000000 --- a/unit_tests/mappings_spec.lua +++ /dev/null @@ -1,117 +0,0 @@ -local mappings = require 'triptych.mappings' -local triptych_config = require 'triptych.config' -local view = require 'triptych.view' -local u = require 'triptych.utils' - -describe('new', function() - it('sets up the expected key bindings', function() - local mock_state = {} - local mock_actions = {} - local mock_refresh = function() end - - local spies = { - keymap_set = {}, - triptych_close = {}, - triptych_get_state = {}, - isdirectory = {}, - view = { - nav_to = {}, - get_target_under_cursor = {}, - jump_to_cwd = {}, - }, - } - - _G.triptych_mock_vim = { - g = { - triptych_config = triptych_config.create_merged_config {}, - triptych_close = function() - table.insert(spies.triptych_close, nil) - end, - triptych_get_state = function() - table.insert(spies.triptych_get_state, nil) - end, - }, - keymap = { - set = function(mode, key_binding, fn, config) - table.insert(spies.keymap_set, { mode, key_binding, fn, config }) - end, - }, - fn = { - isdirectory = function(path) - table.insert(spies.isdirectory, path) - return false - end, - }, - } - - view.set_primary_and_parent_window_targets = function(state, parent_path, diagnostics, git, focused_path) - table.insert(spies.view.nav_to, { state, parent_path, diagnostics, git, focused_path }) - end - view.jump_to_cwd = function(state, diagnostics, git) - table.insert(spies.view.jump_to_cwd, { state, diagnostics, git }) - end - view.get_target_under_cursor = function(state) - table.insert(spies.view.get_target_under_cursor, state) - end - - mappings.new(mock_state, mock_actions, mock_refresh) - - local assert_mapping = function(name, mode) - local results = u.filter(spies.keymap_set, function(entry) - return entry[2] == _G.triptych_mock_vim.g.triptych_config.mappings[name] - end) - assert.same(1, #results) - local result = results[1] - assert.same(mode, result[1]) - assert.same({ buffer = 0, nowait = true }, result[4]) - end - - assert_mapping('nav_left', 'n') - end) - - it('sets up extension mappings', function() - local spies = { - keymap_set = {}, - get_target_under_cursor = {}, - ext_fn = {}, - } - _G.triptych_mock_vim = { - g = { - triptych_config = triptych_config.create_merged_config { - extension_mappings = { - ['xxx'] = { - mode = 'v', - fn = function(target) - table.insert(spies.ext_fn, target) - end, - }, - }, - }, - }, - keymap = { - set = function(mode, key_binding, fn, config) - table.insert(spies.keymap_set, { mode, key_binding, fn, config }) - end, - }, - } - local mock_state = { 'mock_state' } ---@as TriptychState - local mock_target = { 'mock_target' } ---@as PathDetails - local mock_refresh = function() end - view.get_target_under_cursor = function(s) - table.insert(spies.get_target_under_cursor, s) - return mock_target - end - - ---@diagnostic disable-next-line: missing-fields - mappings.new(mock_state, {}, mock_refresh) - local ext_mapping_index = u.list_index_of(spies.keymap_set, function(entry) - return entry[2] == 'xxx' - end) - local ex_mapping = spies.keymap_set[ext_mapping_index] - assert.same(ex_mapping[1], 'v') - assert.same(ex_mapping[2], 'xxx') - ex_mapping[3]() -- Run the mapped function - assert.same({ mock_state }, spies.get_target_under_cursor) - assert.same({ mock_target }, spies.ext_fn) - end) -end) diff --git a/unit_tests/state_spec.lua b/unit_tests/state_spec.lua deleted file mode 100644 index 087440e..0000000 --- a/unit_tests/state_spec.lua +++ /dev/null @@ -1,188 +0,0 @@ -local u = require 'triptych.utils' -local state = require 'triptych.state' -local config = require 'triptych.config' - ----@return PathDetails -local function create_path_details() - return { - path = '/hello/world.js', - display_name = 'world.js', - dirname = '/hello/', - basename = 'world.js', - is_dir = false, - is_git_ignored = false, - } -end - -local conf = config.create_merged_config {} - -describe('new', function() - it('sets initial values', function() - local opening_winid = 4 - local s = state.new(conf, opening_winid) - assert.are.same(s.windows, { - parent = { - path = '', - win = -1, - }, - current = { - previous_path = '', - win = -1, - }, - child = { - win = -1, - }, - }) - assert.are.equal(s.show_hidden, conf.options.show_hidden) - assert.are.same({}, s.cut_list) - assert.are.same({}, s.copy_list) - assert.are.same({}, s.path_to_line_map) - assert.are.equal(opening_winid, s.opening_win) - end) -end) - -describe('list_add', function() - it('adds to the copy list', function() - local s = state.new(conf) - local item = create_path_details() - s:list_add('copy', item) - assert.are.equal(#s.copy_list, 1) - assert.are.same(item, s.copy_list[1]) - end) - - it('adds to the cut list', function() - local s = state.new(conf) - local item = create_path_details() - s:list_add('cut', item) - assert.are.equal(#s.cut_list, 1) - assert.are.same(item, s.cut_list[1]) - end) -end) - -describe('list_remove', function() - it('removes from the copy list', function() - local s = state.new(conf) - local item1 = create_path_details() - local item2 = u.set(item1, 'path', 'foo') - local item3 = u.set(item1, 'path', 'bar') - s:list_add('copy', item1) - s:list_add('copy', item2) - s:list_add('copy', item3) - s:list_remove('copy', item1) - assert.are.same(2, #s.copy_list) - assert.are.same(item2, s.copy_list[1]) - assert.are.same(item3, s.copy_list[2]) - end) - - it('removes from the cut list', function() - local s = state.new(conf) - local item1 = create_path_details() - local item2 = u.set(item1, 'path', 'foo') - local item3 = u.set(item1, 'path', 'bar') - s:list_add('cut', item1) - s:list_add('cut', item2) - s:list_add('cut', item3) - s:list_remove('cut', item1) - assert.are.same(2, #s.cut_list) - assert.are.same(item2, s.cut_list[1]) - assert.are.same(item3, s.cut_list[2]) - end) -end) - -describe('list_remove_all', function() - it('removes all items from the copy list', function() - local item1 = create_path_details() - local item2 = u.set(item1, 'path', 'foo') - local s = state.new(conf) - s:list_add('copy', item1) - s:list_add('copy', item2) - s:list_remove_all 'copy' - assert.are.same(0, #s.copy_list) - end) - - it('removes all items from the cut list', function() - local item1 = create_path_details() - local item2 = u.set(item1, 'path', 'foo') - local s = state.new(conf) - s:list_add('cut', item1) - s:list_add('cut', item2) - s:list_remove_all 'cut' - assert.are.same(0, #s.cut_list) - end) -end) - -describe('list_contains', function() - it('returns true if item is in copy list', function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local item2 = u.set(create_path_details(), 'path', 'bar') - local s = state.new(conf) - s:list_add('copy', item1) - s:list_add('copy', item2) - local result = s:list_contains('copy', item2) - assert.is_true(result) - end) - - it('returns true if item is in cut list', function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local item2 = u.set(create_path_details(), 'path', 'bar') - local s = state.new(conf) - s:list_add('cut', item1) - s:list_add('cut', item2) - local result = s:list_contains('cut', item2) - assert.is_true(result) - end) - - it('returns false if item is not in copy list', function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local item2 = u.set(create_path_details(), 'path', 'bar') - local item3 = u.set(create_path_details(), 'path', 'baz') - local s = state.new(conf) - s:list_add('copy', item1) - s:list_add('copy', item2) - local result = s:list_contains('copy', item3) - assert.is_false(result) - end) - - it('returns false if item is not in cut list', function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local item2 = u.set(create_path_details(), 'path', 'bar') - local item3 = u.set(create_path_details(), 'path', 'baz') - local s = state.new(conf) - s:list_add('cut', item1) - s:list_add('cut', item2) - local result = s:list_contains('cut', item3) - assert.is_false(result) - end) -end) - -describe('list_toggle', function() - it("add item to copy list if it's not already present", function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local s = state.new(conf) - s:list_toggle('copy', item1) - assert.are.same(item1, s.copy_list[1]) - end) - - it("removes item from copy list if it's already present", function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local s = state.new(conf) - s:list_add('copy', item1) - s:list_toggle('copy', item1) - assert.are.same(0, #s.copy_list) - end) - - it("add item to cut list if it's not already present", function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local s = state.new(conf) - s:list_toggle('cut', item1) - assert.are.same(item1, s.cut_list[1]) - end) - - it("removes item from cut list if it's already present", function() - local item1 = u.set(create_path_details(), 'path', 'foo') - local s = state.new(conf) - s:list_add('cut', item1) - s:list_toggle('cut', item1) - assert.are.same(0, #s.cut_list) - end) -end) diff --git a/unit_tests/test_utils.lua b/unit_tests/test_utils.lua deleted file mode 100644 index 519ab10..0000000 --- a/unit_tests/test_utils.lua +++ /dev/null @@ -1,25 +0,0 @@ -local M = {} - ---- Create an iterator ----@param return_values any[] ----@param spy? table -function M.iterator(return_values, spy) - return function(input_value) - local i = 0 - if spy then - table.insert(spy, input_value) - end - return function() - i = i + 1 - local result = return_values[i] - if type(result) == 'table' then - -- This assumes that a table in this case should translate to 2 return values - -- This being a common iteration pattern, like with vim.fs.dir - return result[1], result[2] - end - return result - end - end -end - -return M diff --git a/validate.sh b/validate.sh index fd1b2a3..bd0c584 100755 --- a/validate.sh +++ b/validate.sh @@ -2,16 +2,6 @@ # Use this script to test changes locally -echo "Running unit tests..." -nvim --headless -c 'PlenaryBustedDirectory unit_tests/' -unit_tests_exit_code=$? - -if [ $unit_tests_exit_code -ne 0 ]; then - echo "❌ 1 or more unit tests failed"; -else - echo "✅ Unit tests passed"; -fi - echo "Checking formatting..." npx @johnnymorganz/stylua-bin --check . formatting_exit_code=$? @@ -25,3 +15,13 @@ fi echo "Check diagnostics..." ~/.local/share/nvim/mason/bin/lua-language-server --check . + +echo "Running tests..." +HEADLESS=true nvim --headless +"so%" tests/run_specs.lua +tests_exit_code=$? + +if [ $tests_exit_code -ne 0 ]; then + echo "❌ 1 or more unit tests failed"; +else + echo "✅ Unit tests passed"; +fi