pcall(require, "luacov") local Popup = require("nui.popup") local event = require("nui.utils.autocmd").event local h = require("tests.helpers") local spy = require("luassert.spy") local eq, feedkeys = h.eq, h.feedkeys local function percent(number, percentage) return math.floor(number * percentage / 100) end describe("nui.popup", function() local popup after_each(function() if popup then popup:unmount() popup = nil end end) it("supports o.bufnr (unmanaed buffer)", function() local bufnr = vim.api.nvim_create_buf(false, true) local lines = { "a", "b", "c", } vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) popup = Popup({ bufnr = bufnr, position = "50%", size = { height = "60%", width = "80%", }, }) h.assert_buf_lines(bufnr, lines) eq(popup.bufnr, bufnr) popup:mount() h.assert_buf_lines(bufnr, lines) popup:unmount() eq(popup.bufnr, bufnr) h.assert_buf_lines(bufnr, lines) end) it("accepts number as o.ns_id", function() local ns = "NuiPopupTest" local ns_id = vim.api.nvim_create_namespace(ns) popup = Popup({ ns_id = ns_id, position = "50%", size = { height = "60%", width = "80%", }, }) eq(popup.ns_id, ns_id) end) it("accepts string as o.ns_id", function() local ns = "NuiPopupTest" popup = Popup({ ns_id = ns, position = "50%", size = { height = "60%", width = "80%", }, }) eq(popup.ns_id, vim.api.nvim_create_namespace(ns)) end) it("uses fallback ns_id if o.ns_id=nil", function() popup = Popup({ position = "50%", size = { height = "60%", width = "80%", }, }) eq(type(popup.ns_id), "number") eq(popup.ns_id > 0, true) end) h.describe_flipping_feature("lua_keymap", "method :map", function() it("works before :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:map("n", "l", function() callback() end) popup:mount() feedkeys("l", "x") assert.spy(callback).called() end) it("works after :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "l", function() callback() end) feedkeys("l", "x") assert.spy(callback).called() end) it("supports lhs table", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", { "k", "l" }, "o42") feedkeys("k", "x") feedkeys("l", "x") h.assert_buf_lines(popup.bufnr, { "", "42", "42", }) end) it("supports rhs function", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "l", function() callback() end) feedkeys("l", "x") assert.spy(callback).called() end) it("supports rhs string", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "l", "o42") feedkeys("l", "x") h.assert_buf_lines(popup.bufnr, { "", "42", }) end) it("supports o.remap=true", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "k", "o42") popup:map("n", "l", "k", { remap = true }) feedkeys("k", "x") feedkeys("l", "x") h.assert_buf_lines(popup.bufnr, { "", "42", "42", }) end) it("supports o.remap=false", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "k", "o42") popup:map("n", "l", "k", { remap = false }) feedkeys("k", "x") feedkeys("l", "x") h.assert_buf_lines(popup.bufnr, { "", "42", }) end) it("throws if .bufnr is nil", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup.bufnr = nil local ok, result = pcall(function() popup:map("n", "l", function() end) end) eq(ok, false) eq(type(string.match(result, "buffer not found")), "string") end) end) h.describe_flipping_feature("lua_keymap", "method :unmap", function() it("works before :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:map("n", "l", function() callback() end) popup:unmap("n", "l") popup:mount() feedkeys("l", "x") assert.spy(callback).not_called() end) it("works after :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "l", function() callback() end) popup:unmap("n", "l") feedkeys("l", "x") assert.spy(callback).not_called() end) it("supports lhs string", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "l", "o42") popup:unmap("n", "l") feedkeys("l", "x") h.assert_buf_lines(popup.bufnr, { "", }) end) it("supports lhs table", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:map("n", "k", "o42") popup:map("n", "l", "o42") popup:unmap("n", { "k", "l" }) feedkeys("k", "x") feedkeys("l", "x") h.assert_buf_lines(popup.bufnr, { "", }) end) it("throws if .bufnr is nil", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup.bufnr = nil local ok, result = pcall(function() popup:unmap("n", "l") end) eq(ok, false) eq(type(string.match(result, "buffer not found")), "string") end) end) h.describe_flipping_feature("lua_autocmd", "method :on", function() it("works before :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:on(event.InsertEnter, function() callback() end) popup:mount() feedkeys("i", "x") feedkeys("", "x") assert.spy(callback).called() end) it("works after :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:on(event.InsertEnter, function() callback() end) feedkeys("i", "x") feedkeys("", "x") assert.spy(callback).called() end) it("throws if .bufnr is nil", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup.bufnr = nil local ok, result = pcall(function() popup:on(event.InsertEnter, function() end) end) eq(ok, false) eq(type(string.match(result, "buffer not found")), "string") end) end) h.describe_flipping_feature("lua_autocmd", "method :off", function() it("works before :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:on(event.InsertEnter, function() callback() end) popup:off(event.InsertEnter) popup:mount() feedkeys("i", "x") feedkeys("", "x") assert.spy(callback).not_called() end) it("works after :mount", function() local callback = spy.new(function() end) popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup:mount() popup:on(event.InsertEnter, function() callback() end) popup:off(event.InsertEnter) feedkeys("i", "x") feedkeys("", "x") assert.spy(callback).not_called() end) it("throws if .bufnr is nil", function() popup = Popup({ enter = true, position = "50%", size = { height = "60%", width = "80%", }, }) popup.bufnr = nil local ok, result = pcall(function() popup:off() end) eq(ok, false) eq(type(string.match(result, "buffer not found")), "string") end) end) describe("method :update_layout", function() local function assert_size(size, border_size) if border_size and type(border_size) ~= "table" then border_size = { width = size.width + 2, height = size.height + 2, } end local win_config = vim.api.nvim_win_get_config(popup.winid) eq(win_config.width, size.width) eq(win_config.height, size.height) if popup.border.winid then local border_win_config = vim.api.nvim_win_get_config(popup.border.winid) eq(border_win_config.width, border_size.width) eq(border_win_config.height, border_size.height) end end local function assert_position(position, container_winid) container_winid = container_winid or vim.api.nvim_get_current_win() local win_config = vim.api.nvim_win_get_config(popup.winid) eq(win_config.win, popup.border.winid or container_winid) local row, col = win_config.row[vim.val_idx], win_config.col[vim.val_idx] if popup.border.winid then eq(row, 1) eq(col, 1) local border_win_config = vim.api.nvim_win_get_config(popup.border.winid) local border_row, border_col = border_win_config.row[vim.val_idx], border_win_config.col[vim.val_idx] local border_width, border_height = border_win_config.width, border_win_config.height local delta_width = border_width - win_config.width local delta_height = border_height - win_config.height eq(border_row + math.floor(delta_height / 2 + 0.5), position.row) eq(border_col + math.floor(delta_width / 2 + 0.5), position.col) else eq(row, position.row) eq(col, position.col) end end it("can change size (w/ simple border)", function() local size = { width = 2, height = 1, } popup = Popup({ position = "50%", size = size, }) popup:mount() eq(type(popup.border.winid), "nil") assert_size(size) local new_size = { width = size.width + 2, height = size.height + 2, } popup:update_layout({ size = new_size }) assert_size(new_size) end) it("can change size (w/ complex border)", function() local hl_group = "NuiPopupTest" local style = h.popup.create_border_style_map_with_tuple(hl_group) local size = { width = 2, height = 1, } popup = Popup({ ns_id = vim.api.nvim_create_namespace("NuiTest"), border = { style = style, padding = { 0 }, }, position = "50%", size = size, }) popup:mount() eq(type(popup.border.winid), "number") assert_size(size, true) h.popup.assert_border_lines({ size = size, border = { style = style }, }, popup.border.bufnr) h.popup.assert_border_highlight({ size = size, ns_id = popup.ns_id, }, popup.border.bufnr, hl_group) local new_size = { width = size.width + 2, height = size.height + 2, } popup:update_layout({ size = new_size }) assert_size(new_size, true) h.popup.assert_border_lines({ size = new_size, border = { style = style }, }, popup.border.bufnr) h.popup.assert_border_highlight({ size = new_size, ns_id = popup.ns_id, }, popup.border.bufnr, hl_group) end) it("can change position (w/ simple border)", function() local position = { row = 0, col = 0, } popup = Popup({ position = position, size = { width = 4, height = 2, }, }) popup:mount() eq(type(popup.border.winid), "nil") assert_position(position) local new_position = { row = position.row + 2, col = position.col + 2, } popup:update_layout({ position = new_position }) assert_position(new_position) end) it("can change position (w/ complex border)", function() local hl_group = "NuiPopupTest" local style = h.popup.create_border_style_map_with_tuple(hl_group) local position = { row = 0, col = 0, } popup = Popup({ ns_id = vim.api.nvim_create_namespace("NuiTest"), border = { style = style, padding = { 0 }, }, position = position, size = { width = 4, height = 2, }, }) popup:mount() eq(type(popup.border.winid), "number") assert_position(position) local new_position = { row = position.row + 2, col = position.col + 2, } popup:update_layout({ position = new_position }) assert_position(new_position) end) it("refreshes layout if container size changes", function() local container_size = { width = 20, height = 10, } local container_popup = Popup({ position = 0, size = container_size, }) container_popup:mount() popup = Popup({ relative = { type = "win", winid = container_popup.winid, }, position = "20%", size = "50%", }) popup:mount() assert_size({ width = percent(container_size.width, 50), height = percent(container_size.height, 50), }) assert_position({ row = percent(container_size.height - percent(container_size.height, 50), 20), col = percent(container_size.width - percent(container_size.width, 50), 20), }, container_popup.winid) container_size = { width = 16, height = 8, } container_popup:update_layout({ size = container_size, }) popup:update_layout() assert_size({ width = percent(container_size.width, 50), height = percent(container_size.height, 50), }) assert_position({ row = percent(container_size.height - percent(container_size.height, 50), 20), col = percent(container_size.width - percent(container_size.width, 50), 20), }, container_popup.winid) end) it("throws if missing config 'relative'", function() popup = Popup({}) local ok, result = pcall(function() popup:update_layout({}) end) eq(ok, false) eq(type(string.match(result, "missing layout config: relative")), "string") end) it("throws if missing config 'size'", function() popup = Popup({}) local ok, result = pcall(function() popup:update_layout({ relative = "win", }) end) eq(ok, false) eq(type(string.match(result, "missing layout config: size")), "string") end) it("throws if missing config 'position'", function() popup = Popup({}) local ok, result = pcall(function() popup:update_layout({ relative = "win", size = "50%", }) end) eq(ok, false) eq(type(string.match(result, "missing layout config: position")), "string") end) end) describe("method :mount", function() it("throws if layout is not ready", function() popup = Popup({}) local ok, result = pcall(function() popup:mount() end) eq(ok, false) eq(type(string.match(result, "layout is not ready")), "string") end) it("is idempotent", function() popup = Popup({ position = 0, size = 10, }) local border_mount = spy.on(popup.border, "mount") popup:mount() local bufnr, winid = popup.bufnr, popup.winid eq(type(bufnr), "number") eq(type(winid), "number") assert.spy(border_mount).was_called(1) popup:mount() eq(bufnr, popup.bufnr) eq(winid, popup.winid) assert.spy(border_mount).was_called(1) end) end) h.describe_flipping_feature("lua_autocmd", "method :unmount", function() it("is called when quitted", function() popup = Popup({ position = 0, size = 10, }) local popup_unmount = spy.on(popup, "unmount") popup:mount() vim.api.nvim_buf_call(popup.bufnr, function() vim.cmd([[quit]]) end) vim.wait(100, function() return not popup._.mounted end, 10) assert.spy(popup_unmount).was_called() end) end) h.describe_flipping_feature("lua_autocmd", "method :hide", function() it("works", function() popup = Popup({ position = 0, size = 10, }) popup:mount() vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { "42", }) eq(type(popup.bufnr), "number") eq(type(popup.winid), "number") popup:hide() eq(type(popup.bufnr), "number") eq(type(popup.winid), "nil") h.assert_buf_lines(popup.bufnr, { "42", }) end) it("is idempotent", function() popup = Popup({ position = 0, size = 10, }) popup:mount() local prev_winids = vim.api.nvim_list_wins() popup:hide() local curr_winids = vim.api.nvim_list_wins() eq(#prev_winids, #curr_winids + 1) popup:hide() eq(#curr_winids, #vim.api.nvim_list_wins()) end) it("does nothing if not mounted", function() popup = Popup({ position = 0, size = 10, }) local prev_winids = vim.api.nvim_list_wins() popup:hide() local curr_winids = vim.api.nvim_list_wins() eq(#prev_winids, #curr_winids) end) it("is called when window is closed", function() popup = Popup({ position = 0, size = 10, }) local popup_hide = spy.on(popup, "hide") popup:mount() vim.api.nvim_buf_call(popup.bufnr, function() vim.cmd([[:bdelete]]) end) assert.spy(popup_hide).was_called() end) it("is not called when other popup using same buffer is hidden", function() popup = Popup({ position = 0, size = 10, }) local another_popup = Popup({ bufnr = popup.bufnr, position = 11, size = 5, }) local popup_hide = spy.on(popup, "hide") popup:mount() another_popup:mount() another_popup:hide() assert.spy(popup_hide).was_not_called() another_popup:unmount() end) end) h.describe_flipping_feature("lua_autocmd", "method :show", function() it("works", function() popup = Popup({ position = 0, size = 10, }) popup:mount() vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { "42", }) local bufnr, winid = popup.bufnr, popup.winid eq(type(bufnr), "number") eq(type(winid), "number") popup:hide() popup:show() eq(bufnr, popup.bufnr) eq(type(popup.winid), "number") eq(winid ~= popup.winid, true) h.assert_buf_lines(popup.bufnr, { "42", }) end) it("is idempotent", function() popup = Popup({ position = 0, size = 10, }) popup:mount() popup:hide() local prev_winids = vim.api.nvim_list_wins() popup:show() local curr_winids = vim.api.nvim_list_wins() eq(#prev_winids + 1, #curr_winids) popup:show() eq(#curr_winids, #vim.api.nvim_list_wins()) end) it("does nothing if not mounted", function() popup = Popup({ position = 0, size = 10, }) local prev_winids = vim.api.nvim_list_wins() popup:show() local curr_winids = vim.api.nvim_list_wins() eq(#prev_winids, #curr_winids) end) it("does nothing if not hidden", function() popup = Popup({ position = 0, size = 10, }) popup:mount() local prev_winids = vim.api.nvim_list_wins() popup:show() local curr_winids = vim.api.nvim_list_wins() eq(#prev_winids, #curr_winids) end) it("can show popups using the same buffer", function() popup = Popup({ position = 0, size = 10, }) vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { "42", }) local another_popup = Popup({ bufnr = popup.bufnr, position = 11, size = 5, }) popup:mount() another_popup:mount() local bufnr, winid = popup.bufnr, popup.winid eq(type(bufnr), "number") eq(type(winid), "number") local another_bufnr, another_winid = another_popup.bufnr, another_popup.winid eq(type(another_bufnr), "number") eq(type(another_winid), "number") eq(bufnr, another_bufnr) popup:hide() another_popup:hide() popup:show() another_popup:show() eq(bufnr, popup.bufnr) eq(type(popup.winid), "number") eq(winid ~= popup.winid, true) eq(another_bufnr, another_popup.bufnr) eq(type(another_popup.winid), "number") eq(another_winid ~= another_popup.winid, true) h.assert_buf_lines(bufnr, { "42", }) end) end) end)