diff --git a/.gitignore b/.gitignore index e69de29..4ba7a24 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +tests/.deps diff --git a/Makefile b/Makefile index 19494a6..ef7ccc6 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,8 @@ +clean: + nvim --headless --clean -n -c "lua vim.fn.delete('./tests/.deps', 'rf')" +q test: - nvim --headless --noplugin -u tests/minimal.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'}" + nvim --headless --clean -u tests/test.lua "$(FILE)" +lint: + stylua --check lua/ tests/ +format: + stylua lua/ tests/ diff --git a/lua/nvim-ts-autotag.lua b/lua/nvim-ts-autotag.lua index 51cf99f..b572e1d 100644 --- a/lua/nvim-ts-autotag.lua +++ b/lua/nvim-ts-autotag.lua @@ -16,6 +16,8 @@ function M.init() end end +function M.ensure_ts_parsers_installed() end + function M.setup(opts) internal.setup(opts) vim.cmd([[augroup nvim_ts_xmltag]]) diff --git a/tests/.luarc.json b/tests/.luarc.json new file mode 100644 index 0000000..e18ab58 --- /dev/null +++ b/tests/.luarc.json @@ -0,0 +1,6 @@ +{ + "diagnostics.globals": [ + "it", + "describe" + ] +} \ No newline at end of file diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua new file mode 100644 index 0000000..25f7522 --- /dev/null +++ b/tests/minimal_init.lua @@ -0,0 +1,111 @@ +local M = {} + +local utils = require("tests.utils.utils") +local root = utils.paths.Root:push(".deps/") + +---@class MinPlugin A plugin to download and register on the package path +---@alias PluginName string The plugin name, will be used as part of the git clone destination +---@alias PluginCloneInfo string | string[] The git url a plugin located at or a table of arguments to be passed to `git clone` +---@alias MinPlugins table + +---Downloads a plugin from a given url and registers it on the 'runtimepath' +---@param plugin_name PluginName +---@param plugin_clone_args PluginCloneInfo +function M.load_plugin(plugin_name, plugin_clone_args) + local package_root = root:push("plugins/") + local install_destination = package_root:push(plugin_name):get() + + vim.opt.runtimepath:append(install_destination) + + if not vim.loop.fs_stat(package_root:get()) then + vim.fn.mkdir(package_root:get(), "p") + end + + -- If the plugin install path already exists, we don't need to clone it again. + if not vim.loop.fs_stat(install_destination) then + print(string.format("[LOAD PLUGIN] Downloading plugin '%s' to '%s'", plugin_name, install_destination)) + if type(plugin_clone_args) == "table" then + plugin_clone_args = table.concat(plugin_clone_args, " ") + end + vim.fn.system({ + "git", + "clone", + "--depth=1", + plugin_clone_args, + install_destination, + }) + if vim.v.shell_error > 0 then + error( + string.format("[LOAD PLUGIN] Failed to clone plugin: '%s' to '%s'!", plugin_name, install_destination), + vim.log.levels.ERROR + ) + end + end + print(("[LOAD PLUGIN] Loaded plugin '%s'"):format(plugin_name)) +end + +---Do the initial setup. Downloads plugins, ensures the minimal init does not pollute the filesystem by keeping +---everything self contained to the CWD of the minimal init file. Run prior to running tests, reproducing issues, etc. +---@param plugins? MinPlugins +function M.setup(plugins) + print("[SETUP] Setting up minimal init") + + -- Instead of disabling swap and a bunch of other stuff, we override default xdg locations for + -- Neovim so our test client is as close to a normal client in terms of options as possible + local xdg_root = root:push("xdg") + local std_paths = { + "cache", + "data", + "config", + "state", + } + for _, std_path in pairs(std_paths) do + local xdg_str = "XDG_" .. std_path:upper() .. "_HOME" + local xdg_path = xdg_root:push(std_path):get() + print(("[SETUP] Set vim.env.%-3s -> %s"):format(xdg_str, xdg_path)) + vim.env[xdg_str] = xdg_path + ---@diagnostic disable-next-line: param-type-mismatch + vim.fn.mkdir(vim.fn.stdpath(std_path), "p") + end + + -- Ignore cleanups if specified in the environment + -- NOTE: Cleanup the xdg cache on exit so new runs of the minimal init doesn't share any previous state, e.g. shada + vim.api.nvim_create_autocmd("VimLeave", { + callback = function() + if vim.env.TEST_NO_CLEANUP and vim.env.TEST_NO_CLEANUP:lower() == "true" then + print("[CLEANUP]: `TEST_NO_CLEANUP` was specified, not removing: " .. xdg_root) + else + print("[CLEANUP]: `TEST_NO_CLEANUP` not specified, removing " .. xdg_root) + vim.fn.delete(xdg_root:get(), "rf") + end + end, + }) + + -- Empty the package path so we use only the plugins specified + vim.opt.packpath = {} + + -- Install required plugins + if plugins ~= nil then + for plugin_name, plugin_clone_args in pairs(plugins) do + M.load_plugin(plugin_name, plugin_clone_args) + end + end + + -- Ensure `nvim-ts-autotag` is registed on the runtimepath and set it up + utils.rtp_register_ts_autotag() + require("nvim-ts-autotag").setup({ + enable = true, + enable_rename = true, + enable_close = true, + enable_close_on_slash = true, + }) + print("[SETUP] Finished setting up minimal init") +end + +M.setup({ + ["plenary.nvim"] = "https://github.com/nvim-lua/plenary.nvim", + ["popup.nvim"] = "https://github.com/nvim-lua/popup.nvim", + ["nvim-treesitter"] = "https://github.com/nvim-treesitter/nvim-treesitter", + ["playground"] = "https://github.com/nvim-treesitter/playground", + ["nvim-treesitter-rescript"] = "https://github.com/nkrkv/nvim-treesitter-rescript", +}) diff --git a/tests/close_slash_tag_spec.lua b/tests/specs/close_slash_tag_spec.lua similarity index 96% rename from tests/close_slash_tag_spec.lua rename to tests/specs/close_slash_tag_spec.lua index a81c108..9fa6b66 100644 --- a/tests/close_slash_tag_spec.lua +++ b/tests/specs/close_slash_tag_spec.lua @@ -1,6 +1,6 @@ -local ts = require("nvim-treesitter.configs") -ts.setup({ - ensure_installed = _G.ts_filetypes, +local helpers = require("tests.utils.helpers") + +helpers.setup_nvim_treesitter({ highlight = { enable = true }, }) @@ -171,10 +171,10 @@ local data = { local autotag = require("nvim-ts-autotag") autotag.test = true -local run_data = _G.Test_filter(data) +local run_data = helpers.Test_filter(data) describe("[close slash tag]", function() - _G.Test_withfile(run_data, { + helpers.Test_withfile(run_data, { mode = "i", cursor_add = 0, }) diff --git a/tests/closetag_spec.lua b/tests/specs/closetag_spec.lua similarity index 96% rename from tests/closetag_spec.lua rename to tests/specs/closetag_spec.lua index 90796e5..3212d45 100644 --- a/tests/closetag_spec.lua +++ b/tests/specs/closetag_spec.lua @@ -1,6 +1,6 @@ -local ts = require("nvim-treesitter.configs") -ts.setup({ - ensure_installed = _G.ts_filetypes, +local helpers = require("tests.utils.helpers") + +helpers.setup_nvim_treesitter({ highlight = { enable = true }, }) @@ -209,12 +209,12 @@ local data = { local autotag = require("nvim-ts-autotag") autotag.test = true -local run_data = _G.Test_filter(data) +local run_data = helpers.Test_filter(data) describe("[close tag]", function() - _G.Test_withfile(run_data, { + helpers.Test_withfile(run_data, { mode = "i", cursor_add = 0, - before_each = function(value) end, + before_each = function() end, }) end) diff --git a/tests/renametag_spec.lua b/tests/specs/renametag_spec.lua similarity index 97% rename from tests/renametag_spec.lua rename to tests/specs/renametag_spec.lua index 539cbf6..fc06da5 100644 --- a/tests/renametag_spec.lua +++ b/tests/specs/renametag_spec.lua @@ -1,6 +1,6 @@ -local ts = require("nvim-treesitter.configs") -ts.setup({ - ensure_installed = _G.ts_filetypes, +local helpers = require("tests.utils.helpers") +helpers.setup_nvim_treesitter({ + highlight = { use_languagetree = false, enable = true, @@ -315,11 +315,11 @@ local data = { local autotag = require("nvim-ts-autotag") autotag.test = true -local run_data = _G.Test_filter(data) +local run_data = helpers.Test_filter(data) describe("[rename tag]", function() - _G.Test_withfile(run_data, { + helpers.Test_withfile(run_data, { cursor_add = 0, - before_each = function(value) end, + before_each = function() end, }) end) diff --git a/tests/test.lua b/tests/test.lua new file mode 100644 index 0000000..f7aebcf --- /dev/null +++ b/tests/test.lua @@ -0,0 +1,13 @@ +require("tests.minimal_init") + +---@type string +local test_file = vim.v.argv[#vim.v.argv] +if test_file == "" or not test_file:find("tests/specs/", nil, true) then + test_file = "tests/specs" +end +print("[STARTUP] Running all tests in " .. test_file) + +require("plenary.test_harness").test_directory(test_file, { + minimal_init = "tests/minimal_init.lua", + sequential = true, +}) diff --git a/tests/test-utils.lua b/tests/utils/helpers.lua similarity index 72% rename from tests/test-utils.lua rename to tests/utils/helpers.lua index 11fd88b..4fab3a6 100644 --- a/tests/test-utils.lua +++ b/tests/utils/helpers.lua @@ -1,6 +1,13 @@ +local test_utils = require("tests.utils.utils") + +-- Some helpers depend on utilities from the main plugin, so we have to register the plugin on the +-- path if it isn't already present +test_utils.rtp_register_ts_autotag() + local utils = require("nvim-ts-autotag.utils") local log = require("nvim-ts-autotag._log") -local api = vim.api + +local M = {} local helpers = {} @@ -16,21 +23,36 @@ function helpers.insert(text, is_replace) helpers.feed("i" .. text, "x", is_replace) end -utils.insert_char = function(text) - api.nvim_put({ text }, "c", true, true) +M.insert_char = function(text) + vim.api.nvim_put({ text }, "c", true, true) end -utils.feed = function(text, num) +M.feed = function(text, num) local result = "" for _ = 1, num, 1 do result = result .. text end - api.nvim_feedkeys(api.nvim_replace_termcodes(result, true, false, true), "x", true) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(result, true, false, true), "x", true) end -_G.eq = assert.are.same +M.setup_nvim_treesitter = function(opts) + opts = vim.tbl_deep_extend("keep", opts or {}, { + ensure_installed = { + "html", + "javascript", + "typescript", + "svelte", + "vue", + "tsx", + "php", + "glimmer", + "rescript", + "embedded_template", + }, + }) +end -_G.Test_filter = function(data) +M.Test_filter = function(data) local run_data = {} for _, value in pairs(data) do if value.only == true then @@ -50,34 +72,34 @@ local compare_text = function(linenr, text_after, name, cursor_add, end_cursor) for i = 1, #text_after, 1 do local t = string.gsub(text_after[i], "%|", "") if t and new_text[i] and t:gsub("%s+$", "") ~= new_text[i]:gsub("%s+$", "") then - eq(t, new_text[i], "\n\n text error: " .. name .. "\n") + assert.are.same(t, new_text[i], "\n\n text error: " .. name .. "\n") end local p_after = string.find(text_after[i], "%|") if p_after then local row, col = utils.get_cursor() if end_cursor then - eq(row, linenr + i - 2, "\n\n cursor row error: " .. name .. "\n") - eq(col + 1, end_cursor, "\n\n end cursor column error : " .. name .. "\n") + assert.are.same(row, linenr + i - 2, "\n\n cursor row error: " .. name .. "\n") + assert.are.same(col + 1, end_cursor, "\n\n end cursor column error : " .. name .. "\n") else - eq(row, linenr + i - 2, "\n\n cursor row error: " .. name .. "\n") + assert.are.same(row, linenr + i - 2, "\n\n cursor row error: " .. name .. "\n") p_after = p_after + cursor_add - eq(col, math.max(p_after - 2, 0), "\n\n cursor column error : " .. name .. "\n") + assert.are.same(col, math.max(p_after - 2, 0), "\n\n cursor column error : " .. name .. "\n") end end end return true end -_G.Test_withfile = function(test_data, cb) +M.Test_withfile = function(test_data, cb) for _, value in pairs(test_data) do - it("test " .. value.name, function(done) + it("test " .. value.name, function() local text_before = {} value.linenr = value.linenr or 1 local pos_before = { linenr = value.linenr, colnr = 0, } - if not vim.tbl_islist(value.before) then + if not vim.islist(value.before) then value.before = { value.before } end for index, text in pairs(value.before) do @@ -90,7 +112,7 @@ _G.Test_withfile = function(test_data, cb) end end end - if not vim.tbl_islist(value.after) then + if not vim.islist(value.after) then value.after = { value.after } end vim.bo.filetype = value.filetype or "text" @@ -137,14 +159,14 @@ _G.Test_withfile = function(test_data, cb) end end -_G.dump_node = function(node) +M.dump_node = function(node) local text = utils.get_node_text(node) for _, txt in pairs(text) do log.debug(txt) end end -_G.dump_node_text = function(target) +M.dump_node_text = function(target) for node in target:iter_children() do local node_type = node:type() local text = utils.get_node_text(node) @@ -152,3 +174,4 @@ _G.dump_node_text = function(target) log.debug(text) end end +return M diff --git a/tests/utils/paths.lua b/tests/utils/paths.lua new file mode 100644 index 0000000..25d942a --- /dev/null +++ b/tests/utils/paths.lua @@ -0,0 +1,77 @@ +local M = {} + +---Search up from the current file until we find the full path of the base `tests/` directory and +---return it +---@param test_file string Filename to look for in each directory, if it exists then stop the search +---@return fun(): string dir A function wrapping the found directory +local function search_dir_up(test_file) + -- This is the path of the directory of the current file + local cur_dir = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p") + ---@diagnostic disable-next-line: param-type-mismatch + while not vim.uv.fs_stat(cur_dir .. "/" .. test_file, nil) and cur_dir ~= "/" do + cur_dir = vim.fn.fnamemodify(cur_dir, ":h") + end + if cur_dir == "/" then + error("Failed to locate the base 'tests/' directory!") + end + -- We return a wrapping function instead of a bare string so its easier to enforce "readonly" + -- uses of the searched directory + return function() + return cur_dir + end +end + +--- WARN: DO NOT MUTATE THESE VALUES! +--- +--- Table containing useful paths to directories within the plugin +M.static = { + --- Full path of the `tests/` directory + tests_dir = search_dir_up("test.lua"), + --- Full base path of the plugin where the `.git` directory resides + ts_autotag_dir = search_dir_up(".git"), +} + +---@class nvim-ts-autotag.Root +---@field package path string +local Root = { + path = M.static.tests_dir(), +} + +---@package +---@nodiscard +---@return nvim-ts-autotag.Root +function Root:new(o) + o = o or {} + + setmetatable(o, self) + -- The last part of the path must be a "/" to work correctly + if self.path:sub(#self.path, #self.path) ~= "/" then + self.path = self.path .. "/" + end + self.__index = self + return o +end + +--- Create a new instance, pushing the given path onto it +---@nodiscard +---@param path string The new path to add +---@return nvim-ts-autotag.Root +function Root:push(path) + local new_root = self:new() + -- Since we're extending the path, the current path must be a directory, ensure it ends in a "/" + if new_root.path:sub(#new_root.path) ~= "/" then + new_root.path = new_root.path .. "/" + end + new_root.path = new_root.path .. path + return new_root +end + +--- Get the current path string +---@return string path +function Root:get() + return self.path +end + +M.Root = Root:new() + +return M diff --git a/tests/utils/utils.lua b/tests/utils/utils.lua new file mode 100644 index 0000000..dc9b322 --- /dev/null +++ b/tests/utils/utils.lua @@ -0,0 +1,14 @@ +local path_utils = require("tests.utils.paths") +local M = {} + +M.paths = path_utils + +--- Register the main plugin (`nvim-ts-autotag`) on the runtimepath if it hasn't already been +--- registered +M.rtp_register_ts_autotag = function() + if not vim.list_contains(vim.opt.runtimepath, path_utils.static.ts_autotag_dir()) then + vim.opt.runtimepath:append(path_utils.static.ts_autotag_dir()) + end +end + +return M