--- JQuote Unit Tests --- @author Jan --- @license MIT -- Create global state for mock local test_state = { current_line = "", cursor_row = 1, cursor_col = 0, notifications = {}, keymaps = {} } -- Mock vim API for testing local mock_vim = { log = { levels = { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, } }, notify = function(message, level) table.insert(test_state.notifications, { message = message, level = level }) end, api = { nvim_get_current_line = function() return test_state.current_line end, nvim_win_get_cursor = function() return { test_state.cursor_row, test_state.cursor_col } end, nvim_set_current_line = function(line) test_state.current_line = line return true end, nvim_win_set_cursor = function(win, pos) test_state.cursor_row = pos[1] test_state.cursor_col = pos[2] return true end, }, keymap = { set = function(mode, key, func, opts) test_state.keymaps[key] = { mode = mode, func = func, opts = opts } return true end, }, tbl_deep_extend = function(behavior, ...) local result = {} for _, tbl in ipairs({...}) do for k, v in pairs(tbl) do if type(v) == "table" and type(result[k]) == "table" then result[k] = mock_vim.tbl_deep_extend(behavior, result[k], v) else result[k] = v end end end return result end, deepcopy = nil, -- Will be set after mock_vim is defined -- Test utilities _reset = function() test_state.current_line = "" test_state.cursor_row = 1 test_state.cursor_col = 0 test_state.notifications = {} test_state.keymaps = {} end, _get_notifications = function() return test_state.notifications end, _set_line_and_cursor = function(line, col) test_state.current_line = line test_state.cursor_col = col or 0 end, _keymaps = test_state.keymaps } -- Set up deepcopy function now that mock_vim is defined mock_vim.deepcopy = function(tbl) if type(tbl) ~= "table" then return tbl end local copy = {} for k, v in pairs(tbl) do if type(v) == "table" then copy[k] = mock_vim.deepcopy(v) else copy[k] = v end end return copy end -- Add current directory to Lua path for module loading package.path = package.path .. ";./?.lua;./?/init.lua" -- Inject mock vim into global scope BEFORE loading the module _G.vim = mock_vim -- Load the module under test local jquote = require('lua.jquote.init') -- Test suite describe("JQuote Plugin", function() before_each(function() mock_vim._reset() -- Reset plugin to default state jquote.setup({}) end) describe("Plugin Initialization", function() it("should initialize with default options", function() local success = jquote.setup() assert.is_true(success) assert.are.equal("tq", jquote.options.hotkey) assert.are.same({ "'", '"', "`" }, jquote.options.quote_chars) assert.are.equal("auto", jquote.options.jump_behavior) end) it("should merge user options with defaults", function() local user_options = { hotkey = "q", quote_chars = { "'", '"' }, jump_behavior = "manual" } local success = jquote.setup(user_options) assert.is_true(success) assert.are.equal("q", jquote.options.hotkey) assert.are.same({ "'", '"' }, jquote.options.quote_chars) assert.are.equal("manual", jquote.options.jump_behavior) end) it("should validate quote_chars configuration", function() local success = jquote.setup({ quote_chars = {} }) assert.is_false(success) local notifications = mock_vim._get_notifications() assert.is_true(#notifications > 0) assert.matches("quote_chars must contain at least", notifications[1].message) end) it("should validate jump_behavior configuration", function() local success = jquote.setup({ jump_behavior = "invalid" }) assert.is_false(success) local notifications = mock_vim._get_notifications() assert.is_true(#notifications > 0) assert.matches("jump_behavior must be either", notifications[1].message) end) it("should set up key mapping", function() jquote.setup({ hotkey = "test" }) assert.is_not_nil(mock_vim._keymaps["test"]) assert.are.equal("n", mock_vim._keymaps["test"].mode) end) end) describe("Quote Detection", function() it("should find quoted string at cursor position", function() mock_vim._set_line_and_cursor("hello 'world' test", 8) local success = jquote.toggle_quotes() assert.is_true(success) assert.are.equal('hello "world" test', test_state.current_line) end) it("should cycle through quote characters", function() jquote.setup({ quote_chars = { "'", '"', "`" } }) -- First toggle: ' -> " mock_vim._set_line_and_cursor("hello 'world'", 8) jquote.toggle_quotes() assert.are.equal('hello "world"', test_state.current_line) -- Second toggle: " -> ` mock_vim._set_line_and_cursor('hello "world"', 8) jquote.toggle_quotes() assert.are.equal('hello `world`', test_state.current_line) -- Third toggle: ` -> ' mock_vim._set_line_and_cursor('hello `world`', 8) jquote.toggle_quotes() assert.are.equal("hello 'world'", test_state.current_line) end) it("should handle nested quotes correctly", function() mock_vim._set_line_and_cursor('outer "inner \'nested\' text" end', 15) jquote.toggle_quotes() assert.are.equal('outer "inner "nested" text" end', test_state.current_line) end) it("should handle multiple quote pairs on same line", function() mock_vim._set_line_and_cursor("'first' and 'second' pair", 2) jquote.toggle_quotes() assert.are.equal('"first" and \'second\' pair', test_state.current_line) end) end) describe("Auto Jump Functionality", function() it("should jump to next quote pair when cursor is not inside quotes", function() jquote.setup({ jump_behavior = "auto" }) mock_vim._set_line_and_cursor("hello 'world' and 'test'", 0) local success = jquote.toggle_quotes() assert.is_true(success) assert.are.equal(6, test_state.cursor_col) -- Should jump to first quote end) it("should not jump when jump_behavior is manual", function() jquote.setup({ jump_behavior = "manual" }) mock_vim._set_line_and_cursor("hello 'world' test", 0) local success = jquote.toggle_quotes() assert.is_false(success) assert.are.equal(0, test_state.cursor_col) -- Cursor should not move end) it("should handle case with no quote pairs found", function() jquote.setup({ jump_behavior = "auto" }) mock_vim._set_line_and_cursor("no quotes here", 0) local success = jquote.toggle_quotes() assert.is_false(success) local notifications = mock_vim._get_notifications() assert.is_true(#notifications > 0) assert.matches("No quote pairs found", notifications[1].message) end) end) describe("Error Handling", function() it("should handle invalid line content gracefully", function() -- Mock API failure mock_vim.api.nvim_get_current_line = function() error("API Error") end local success = jquote.toggle_quotes() assert.is_false(success) local notifications = mock_vim._get_notifications() assert.is_true(#notifications > 0) assert.matches("Failed to get current line", notifications[1].message) end) it("should handle cursor position API failure", function() mock_vim.api.nvim_win_get_cursor = function() error("Cursor API Error") end local success = jquote.toggle_quotes() assert.is_false(success) local notifications = mock_vim._get_notifications() assert.is_true(#notifications > 0) assert.matches("Failed to get cursor position", notifications[1].message) end) it("should handle line update failure", function() mock_vim._set_line_and_cursor("'test'", 2) mock_vim.api.nvim_set_current_line = function() error("Set line error") end local success = jquote.toggle_quotes() assert.is_false(success) local notifications = mock_vim._get_notifications() assert.is_true(#notifications > 0) assert.matches("Failed to update line", notifications[1].message) end) it("should handle excessively long lines", function() local long_line = string.rep("a", 15000) .. "'test'" -- Over MAX_LINE_LENGTH mock_vim._set_line_and_cursor(long_line, 10000) local success = jquote.toggle_quotes() assert.is_false(success) end) end) describe("Version Management", function() it("should return version from git tag", function() -- Mock io.popen to return a version local original_io_popen = io.popen io.popen = function(cmd) return { read = function() return "v1.2.3\n" end, close = function() return true end } end local version = jquote.get_version() assert.are.equal("1.2.3", version) -- Restore original function io.popen = original_io_popen end) it("should return fallback version when git fails", function() local original_io_popen = io.popen io.popen = function(cmd) error("Git not available") end local version = jquote.get_version() assert.are.equal("unknown", version) io.popen = original_io_popen end) it("should validate semantic version format", function() local original_io_popen = io.popen io.popen = function(cmd) return { read = function() return "invalid-version\n" end, close = function() return true end } end local version = jquote.get_version() assert.are.equal("unknown", version) io.popen = original_io_popen end) end) describe("Health Check", function() it("should return comprehensive health information", function() jquote.setup({ hotkey = "test", quote_chars = { "'", '"' }, jump_behavior = "auto" }) local health = jquote.health() assert.is_table(health.options) assert.are.equal(2, health.quote_chars_count) assert.is_true(health.hotkey_configured) assert.is_true(health.jump_behavior_valid) assert.is_string(health.plugin_version) end) it("should detect invalid configuration in health check", function() -- Manually corrupt the configuration jquote.options.jump_behavior = "invalid" local health = jquote.health() assert.is_false(health.jump_behavior_valid) end) end) describe("Edge Cases", function() it("should handle empty quote_chars gracefully", function() -- Manually set invalid state to test defensive programming jquote.options.quote_chars = {} mock_vim._set_line_and_cursor("'test'", 2) local success = jquote.toggle_quotes() assert.is_false(success) end) it("should handle quote at line boundaries", function() mock_vim._set_line_and_cursor("'start", 0) local success = jquote.toggle_quotes() assert.is_false(success) -- No closing quote end) it("should handle cursor at exact quote position", function() mock_vim._set_line_and_cursor("'test'", 0) -- Cursor on opening quote local success = jquote.toggle_quotes() assert.is_true(success) assert.are.equal('"test"', test_state.current_line) end) it("should handle single character quoted strings", function() mock_vim._set_line_and_cursor("'a'", 1) local success = jquote.toggle_quotes() assert.is_true(success) assert.are.equal('"a"', test_state.current_line) end) end) end)