telescope.nvim icon indicating copy to clipboard operation
telescope.nvim copied to clipboard

Add documentation on how to write tests for telescope.nvim

Open ColinKennedy opened this issue 1 year ago • 2 comments

Is your feature request related to a problem? Please describe. I'm writing an extension and, despite checking every extension from the wiki, I couldn't find any that actually runs the their code via telescope calls. The best I saw was a couple plugins test their own internal logic and don't even attempt to call their code the way a user might.

Describe the solution you'd like Be able to call a picker and query its contents in a unittest / integration test.

  it("shows up with 1 saved session", function()
    -- Some prologue code that could affect `assert.equal` later

    local picker = viewer.create()
    picker:find()
    assert.equal(1, picker.manager:num_results())  -- Some meaningful check
  end)

Describe alternatives you've considered n/a

ColinKennedy avatar Apr 14 '24 00:04 ColinKennedy

Probably the closest thing to this would be these tests https://github.com/nvim-telescope/telescope.nvim/blob/master/lua/tests/automated/pickers/find_files_spec.lua

But even in our code base, its usage is very limited and hasn't been used for anything but some common operations for the find_files picker. I tried writing some integration tests for another extension previously but I found it quite tricky (for my usecase). I would like to improve this aspect but I'm constrained for time.

jamestrew avatar Apr 20 '24 03:04 jamestrew

I decided to try this again and found a working solution, at least for my use case. At some point I'll publish the code but for now, this is a taste of it:

A working example
local action_state = require("telescope.actions.state")
local action_utils = require("telescope.actions.utils")
local entry_display = require("telescope.pickers.entry_display")
local finders = require("telescope.finders")
local pickers = require("telescope.pickers")
local telescope_actions = require("telescope.actions")
local telescope_config = require("telescope.config").values


local _ORIGINAL_GET_SELECTION_FUNCTION = nil
local _RESULT = nil

local function _get_selection(buffer)
    local books = {}

    action_utils.map_selections(buffer, function(selection)
        table.insert(books, selection.value)
    end)

    if not vim.tbl_isempty(books) then
        return books
    end

    local selection = action_state.get_selected_entry()

    if selection ~= nil then
        return { selection.value }
    end

    return {}
end

local function _mock_after()
    _get_selection = _ORIGINAL_GET_SELECTION_FUNCTION
    _RESULT = nil
end

local function _mock_get_selection(caller)
    _ORIGINAL_GET_SELECTION_FUNCTION = caller

    _get_selection = function(...)
        local selection = caller(...)
        _RESULT = selection

        return selection
    end
end

local function _get_picker()
    local function _select_book(buffer)
        local selection = _get_selection(buffer)

        if vim.tbl_isempty(selection) then
            vim.notify("No selection could be found", vim.log.levels.ERROR)
            telescope_actions.close(buffer)

            return
        end

        print("Selected books:")

        for _, book in ipairs(selection) do
            print(book)
        end

        telescope_actions.close(buffer)
    end

    local displayer = entry_display.create({
        separator = " ",
        items = {
            { width = 0.8 },
            { remaining = true },
        },
    })

    local books = {
        { "Guns, Germs, and Steel: The Fates of Human Societies", "Jared M. Diamond" },
        { "Herodotus Histories", "Herodotus" },
        { "The Origin of Consciousness in the Breakdown of the Bicameral Mind", "Julian Jaynes" },
        { "What Every Programmer Should Know About Memory", "Ulrich Drepper" },
        { "When: The Scientific Secrets of Perfect Timing", "Daniel H. Pinker" },
    }

    local options = {}

    local picker = pickers
        .new(options, {
            prompt_title = "Choose A Book",
            finder = finders.new_table({
                results = books,
                entry_maker = function(data)
                    local name, author = unpack(data)
                    local value = string.format("%s - %s", name, author)

                    return {
                        display = function(entry)
                            return displayer({
                                { entry.name, "PluginTemplateTelescopeEntry" },
                                { entry.author, "PluginTemplateTelescopeSecondary" },
                            })
                        end,
                        author = author,
                        name = name,
                        value = value,
                        ordinal = value,
                    }
                end,
            }),
            previewer = false,
            sorter = telescope_config.generic_sorter(options),
            attach_mappings = function()
                telescope_actions.select_default:replace(_select_book)

                return true
            end,
        })

    return picker
end

local function main()

    local function _wait_for_picker_to_initialize()
        local initialized = false

        vim.schedule(function() initialized = true end)

        vim.wait(1000, function() return initialized end)
    end

    local picker = _get_picker()

    _mock_get_selection(_get_selection)
    picker:find()
    _wait_for_picker_to_initialize()
    picker:set_selection(picker.max_results - picker.manager:num_results())
    picker:toggle_selection(picker:get_selection_row())
    picker:move_selection(1)
    picker:toggle_selection(picker:get_selection_row())

    require("telescope.actions").select_default(vim.api.nvim_get_current_buf())

    print("FINAL RESULT")
    print(vim.inspect(_RESULT))

    _mock_after()
end

main()

The jist is that telescope.nvim uses plenary to schedule tasks and plenary is mostly a wrap around vim.schedule. Since vim.schedule is just a single queue + event loop, So as long as you make your code wait until the end of the queue is exhausted, you can be sure that the telescope floating buffer is ready. If you later do some more stuff and are worried that the Picker floating buffer is out of sync again, just run _wait_for_picker_to_initialize() before trying to access anything. It works for my use cases so far, it seems. I did try some experiments like changing _wait_for_picker_to_initialize() to be a "wait for picker.manager to exist" and other guesses but it seemed not to matter what I did. Just a simple wait was enough.

One curious part though is picker:set_selection(picker.max_results - picker.manager:num_results()). My picker has 5 items in it so I was expecting to be able to call picker:set_selection(1), picker:set_selection(2), picker:set_selection(3), etc but it looks like the row numbers are absolute values and based off of picker.max_results. Is that normal or did I fail to initialize something properly? Don't know!

Anyway as long as the selection is set, the rest has been smooth sailing. One unsolved problem is that I'd really ideally like to call Neo/vim feedkeys, raw, instead of calling telescope's picker API every time I want to interact with the buffer. I tried out things like vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>",true,false,true), "m", false) to emulate a Enter key press but it just never worked as expected. If someone works out those details I'd be super happy to hear about it!

I still think this repo could use an example / docs on the expected way to test it but at least we now have a starting point. It has been working for me so far, finger's crossed.

ColinKennedy avatar Aug 27 '24 03:08 ColinKennedy