quolpr/quicktest.nvim

github github
test
stars 76
issues 3
subscribers 4
forks 5
CREATED

2024-06-16

UPDATED

4 days ago


Quicktest

  • Contextual Test Triggering: Run tests directly from where your cursor is located or execute all tests in the entire file/dir/project.
  • Flexible Test Reruns: Rerun tests from any location(with require('quicktest').run_previous(), keybind is in usage example), automatically opening window or using an existing if it's open.
  • Live-Scrolling Results: Continuously scroll through test results as they are generated. But stop scrolling if you decided to scroll up.
  • Real-Time Feedback: View the results of tests immediately as they run, without waiting for the completion of the test suite.
  • Test Duration Timer: Display a timer to monitor the duration of ongoing tests.
  • ANSI colors EXPERIMETAL
  • Easy to write your own adapter: It's just all about running cmd and piping results to quicktest.

https://github.com/user-attachments/assets/9fcb3e17-f521-4660-9d9a-d9f763de5a1b

Installation

With Lazy:

{
  "quolpr/quicktest.nvim",
  config = function()
    local qt = require("quicktest")

    qt.setup({
      -- Choose your adapter, here all supported adapters are listed
      adapters = {
        require("quicktest.adapters.golang")({}),
        require("quicktest.adapters.vitest")({}),
        require("quicktest.adapters.playwright")({}),
        require("quicktest.adapters.elixir"),
        require("quicktest.adapters.criterion"),
        require("quicktest.adapters.dart"),
      },
      -- split or popup mode, when argument not specified
      default_win_mode = "split",
      use_experimental_colorizer = true
    })
  end,
  dependencies = {
    "nvim-lua/plenary.nvim",
    "MunifTanjim/nui.nvim",
  },
  keys = {
    {
      "<leader>tl",
      function()
        local qt = require("quicktest")
        -- current_win_mode return currently opened panel, split or popup
        qt.run_line()
        -- You can force open split or popup like this:
        -- qt.run_line('split')
        -- qt.run_line('popup')
      end,
      desc = "[T]est Run [L]line",
    },
    {
      "<leader>tf",
      function()
        local qt = require("quicktest")

        qt.run_file()
      end,
      desc = "[T]est Run [F]ile",
    },
    {
      '<leader>td',
      function()
        local qt = require 'quicktest'

        qt.run_dir()
      end,
      desc = '[T]est Run [D]ir',
    },
    {
      '<leader>ta',
      function()
        local qt = require 'quicktest'

        qt.run_all()
      end,
      desc = '[T]est Run [A]ll',
    },
    {
      "<leader>tp",
      function()
        local qt = require("quicktest")

        qt.run_previous()
      end,
      desc = "[T]est Run [P]revious",
    },
    {
      "<leader>tt",
      function()
        local qt = require("quicktest")

        qt.toggle_win("split")
      end,
      desc = "[T]est [T]oggle Window",
    },
    {
      "<leader>tc",
      function()
        local qt = require("quicktest")

        qt.cancel_current_run()
      end,
      desc = "[T]est [C]ancel Current Run",
    },
  },
}

Without Lazy:

local qt = require("quicktest")

-- Choose your adapter, here all supported adapters are listed
qt.setup({
  adapters = {
    require("quicktest.adapters.golang"),
    require("quicktest.adapters.vitest")({}),
    require("quicktest.adapters.playwright")({}),
    require("quicktest.adapters.elixir"),
    require("quicktest.adapters.criterion"),
    require("quicktest.adapters.dart"),
  },
  -- split or popup mode, when argument not specified
  default_win_mode = "split",
  use_experimental_colorizer = true
})

vim.keymap.set("n", "<leader>tl", qt.run_line, {
  desc = "[T]est Run [L]line",
})
vim.keymap.set("n", "<leader>tf", qt.run_file, {
  desc = "[T]est Run [F]ile",
})
vim.keymap.set("n", "<leader>td", qt.run_dir, {
  desc = "[T]est Run [D]ir",
})
vim.keymap.set("n", "<leader>ta", qt.run_all, {
  desc = "[T]est Run [A]ll",
})
vim.keymap.set("n", "<leader>tR", qt.run_previous, {
  desc = "[T]est Run [P]revious",
})
-- vim.keymap.set("n", "<leader>tt", function()
--   qt.toggle_win("popup")
-- end, {
--   desc = "[T]est [T]oggle popup window",
-- })
vim.keymap.set("n", "<leader>tt", function()
  qt.toggle_win("split")
end, {
  desc = "[T]est [T]oggle Window",
})
vim.keymap.set("n", "<leader>tc", function()
  qt.cancel_current_run()
end, {
  desc = "[T]est [C]ancel Current Run",
})

Commands

:QuicktestRun[Line/File/Dir/All] <win_mode> <adapter> ...<args>

Examples:

:QuicktestRunLine auto auto --my=arg
:QuicktestRunLine popup auto --my=arg
:QuicktestRunLine split auto --my=arg
:QuicktestRunLine split go --my=arg


:QuicktestRunFile split go --my=arg
:QuicktestRunDir split go --my=arg
:QuicktestRunAll split go --my=arg

Api

local qt = require 'quicktest'

-- Choose your adapter, here all supported adapters are listed
qt.setup({
  adapters = {
    require("quicktest.adapters.golang")({
      ---@field cwd (fun(bufnr: integer, current: string?): string)?
      ---@field bin (fun(bufnr: integer, current: string?): string)?
      ---@field additional_args (fun(bufnr: integer): string[])?
      ---@field args (fun(bufnr: integer, current: string[]): string[])?
      ---@field env (fun(bufnr: integer, current: table<string, string>): table<string, string>)?
      ---@field is_enabled (fun(bufnr: integer, type: RunType, current: boolean): boolean)?

      additional_args = function(bufnr) return { '-race', '-count=1' } end
      -- bin = function(bufnr, current) return current end
      -- cwd = function(bufnr, current) return current end
    }),
    require("quicktest.adapters.vitest")({
      ---@class VitestAdapterOptions
      ---@field cwd (fun(bufnr: integer, current: string?): string)?
      ---@field bin (fun(bufnr: integer, current: string?): string)?
      ---@field config_path (fun(bufnr: integer, current: string): string)?
      ---@field args (fun(bufnr: integer, current: string[]): string[])?
      ---@field env (fun(bufnr: integer, current: table<string, string>): table<string, string>)?
      ---@field is_enabled (fun(bufnr: integer, type: RunType, current: boolean): boolean)?

      -- bin = function(bufnr, current) return current end
      -- cwd = function(bufnr, current) return current end
      -- config_path = function(bufnr, current) return current end
    }),
    require("quicktest.adapters.elixir")({
      ---@class ElixirAdapterOptions
      ---@field cwd (fun(bufnr: integer, current: string?): string)?
      ---@field bin (fun(bufnr: integer, current: string?): string)?
      ---@field args (fun(bufnr: integer, current: string[]): string[])?
      ---@field env (fun(bufnr: integer, current: table<string, string>): table<string, string>)?
      ---@field is_enabled (fun(bufnr: integer, type: RunType, current: boolean): boolean)?
    }),
    require("quicktest.adapters.playwright")({
      ---@class PlaywrightAdapterOptions
      ---@field cwd (fun(bufnr: integer, current: string?): string)?
      ---@field bin (fun(bufnr: integer, current: string?): string)?
      ---@field config_path (fun(bufnr: integer, current: string): string)?
      ---@field args (fun(bufnr: integer, current: string[]): string[])?
      ---@field env (fun(bufnr: integer, current: table<string, string>): table<string, string>)?
      ---@field is_enabled (fun(bufnr: integer, type: RunType, current: boolean): boolean)?
    }),
    require("quicktest.adapters.elixir")({
      ---@class ElixirAdapterOptions
      ---@field cwd (fun(bufnr: integer, current: string?): string)?
      ---@field bin (fun(bufnr: integer, current: string?): string)?
      ---@field args (fun(bufnr: integer, current: string[]): string[])?
      ---@field env (fun(bufnr: integer, current: table<string, string>): table<string, string>)?
      ---@field is_enabled (fun(bufnr: integer, type: RunType, current: boolean): boolean)?
    }),
    require("quicktest.adapters.criterion")({
      builddir = function(bufnr) return "build" end,
      additional_args = function(bufnr) return {'arg1', 'arg2'} end,
    }),
    require("quicktest.adapters.dart")({
      ---@class DartAdapterOptions
      ---@field cwd (fun(bufnr: integer, current: string?): string)?
      ---@field bin (fun(bufnr: integer, current: string?): string)?
      ---@field args (fun(bufnr: integer, current: string[]): string[])?
      ---@field env (fun(bufnr: integer, current: table<string, string>): table<string, string>)?
      ---@field is_enabled (fun(bufnr: integer, type: RunType, current: boolean): boolean)?
    }),
  },
  -- split or popup mode, when argument not specified
  default_win_mode = "split",
  -- Baleia make coloured output. Requires baleia package. Can cause crashes https://github.com/quolpr/quicktest.nvim/issues/11
  use_baleia = false
})

-- Find nearest test under cursor and run in popup
qt.run_line('popup')
-- Find nearest test under cursor and run in split
qt.run_line('split')
-- Find nearest test under cursor and run in currently opened window(popup or split)
qt.run_line()

-- Run all tests of file in popup/split
qt.run_file('popup')
qt.run_file('split')
qt.run_line()

-- Run all tests of current file dir in popup/split
qt.run_dir('popup')
qt.run_dir('split')
qt.run_dir()

-- Run all tests of project in popup/split
qt.run_all('popup')
qt.run_all('split')
qt.run_all()

-- Open or close split/popup if already opened, without running tests.
-- Just open and close window.
qt.toggle_win('popup')
qt.toggle_win('split')

-- Take previous test run and run in popup/split
qt.run_previous('popup')
qt.run_previous('split')
qt.run_previous()

Languages with multiple adapters

Same languages like Javascript/Typescript support multiple adapters that might match the same test file. Use the is_enabled option to control which adapter should be used for the current buffer.

Some adapters like playwright and vitest provide a helper function to determine whether the current buffer imports from a certain package like @playwright or vitest. Here is a sample configuration for a project with Playwright and vitest tests:

local qt = require("quicktest")
local playwright = require("quicktest.adapters.playwright")
local vitest = require("quicktest.adapters.vitest")

qt.setup({
  adapters = {
    vitest({
      is_enabled = function(bufnr)
        return vitest.imports_from_vitest(bufnr)
      end
    }),
    playwright({
      is_enabled = function(bufnr)
        -- In case you are not using the default `@playwright` package but your own
        -- wrapper, you can specify the package-name that has to be imported
        return playwright.imports_from_playwright(bufnr, "my-custom-playwright")
      end
    }),
  },
})

Screenshots

Running test in split window

Running test in popup window

Canceled test

Building your own adapter

Here is the template of how adapter for any language could be written. For more examples just check lua/quicktest/adapters. For tresitter methods investigation you can take code from adapters of neotest from https://github.com/nvim-neotest/neotest?tab=readme-ov-file#supported-runners

local Job = require("plenary.job")

local M = {
  name = "myadapter",
}
---@class MyRunParams
---@field func_names string[]
---@field bufnr integer
---@field cursor_pos integer[]

--- Optional:
--- Builds parameters for running tests based on buffer number and cursor position.
--- This function should be customized to extract necessary information from the buffer.
---@param bufnr integer
---@param cursor_pos integer[]
---@return MyRunParams, nil | string
-- M.build_line_run_params = function(bufnr, cursor_pos)
--   -- You can get current function name to run based on bufnr and cursor_pos
--   -- Check hot it is done for golang at `lua/quicktest/adapters/golang`
--   return {
--     bufnr = bufnr,
--     cursor_pos = cursor_pos,
--     func_names = {},
--     -- Add other parameters as needed
--   }, nil
-- end

--- Optional:
---@param bufnr integer
---@param cursor_pos integer[]
---@return MyRunParams, nil | string
-- M.build_file_run_params = function(bufnr, cursor_pos)
--   return {
--     bufnr = bufnr,
--     cursor_pos = cursor_pos,
--     -- Add other parameters as needed
--   }, nil
-- end

--- Optional:
---@param bufnr integer
---@param cursor_pos integer[]
---@return MyRunParams, nil | string
-- M.build_dir_run_params = function(bufnr, cursor_pos)
--   return {
--     bufnr = bufnr,
--     cursor_pos = cursor_pos,
--     -- Add other parameters as needed
--   }, nil
-- end

--- Optional:
---@param bufnr integer
---@param cursor_pos integer[]
---@return MyRunParams, nil | string
-- M.build_all_run_params = function(bufnr, cursor_pos)
--   return {
--     bufnr = bufnr,
--     cursor_pos = cursor_pos,
--     -- Add other parameters as needed
--   }, nil
-- end

--- Executes the test with the given parameters.
---@param params MyRunParams
---@param send fun(data: any)
---@return integer
M.run = function(params, send)
  local job = Job:new({
    command = "test_command",
    args = { "--some-flag" }, -- Modify based on how your test command needs to be structured
    on_stdout = function(_, data)
      send({ type = "stdout", output = data })
    end,
    on_stderr = function(_, data)
      send({ type = "stderr", output = data })
    end,
    on_exit = function(_, return_val)
      send({ type = "exit", code = return_val })
    end,
  })

  job:start()

  return job.pid
end

--- Optional: title of the test run
---@param params MyRunParams
-- M.title = function(params)
--   return "Running test"
-- end

--- Optional: handles actions to take after the test run, based on the results.
---@param params any
---@param results any
-- M.after_run = function(params, results)
--   -- Implement actions based on the results, such as updating UI or handling errors
-- end

--- Checks if the adapter is enabled for the given buffer.
---@param bufnr integer
---@return boolean
M.is_enabled = function(bufnr)
  local bufname = vim.api.nvim_buf_get_name(bufnr)
  return vim.endswith(bufname, "test.ts") or vim.endswith(bufname, "test.js")
end

return M