Quicktest improves your testing experience in real-time with flexible display options like popups or split windows, customized to your workflow preferences. Key features include identifying the nearest function and triggering its test, rerunning previous tests from any location, and live scrolling of results alongside a running timer for immediate feedback.
Currently supported languages: Go, Typescript/Javascript(vitest and Playwright), Elixir, Dart, C(criterion). There is also a template in Readme below about how to create own adapter. Should be pretty easy, just shell command execute!
example.mp4
- 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()
), 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: Just supported.
- Easy API for adapters: It's just all about running cmd and piping results to
quicktest
.
If these features resonate with you, Quicktest might be just what you need!
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()
: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
Supported languages: Go, Typescript/Javascript(vitest and Playwright), C (criterion with meson), Dart
Feel free to open PR for your language, the plugin API is pretty simple and described in Building your own plugin
section in this Readme.
Simple configurations:
local qt = require("quicktest")
-- Choose your adapter, here all supported adapters are listed
qt.setup({
adapters = {
require("quicktest.adapters.golang"),
require("quicktest.adapters.vitest")({
-- bin = function(bufnr) return 'vitest' end
-- cwd = function(bufnr) return bufnr end
-- config_path = function(bufnr) return 'vitest.config.js' end
}),
require("quicktest.adapters.playwright")({
-- bin = function(bufnr) return 'vitest' end
-- cwd = function(bufnr) return bufnr end
-- config_path = function(bufnr) return 'vitest.config.js' end
}),
require("quicktest.adapters.elixir"),
require("quicktest.adapters.criterion"),
require("quicktest.adapters.dart"),
},
-- 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
})
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",
})
Using 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")({
additional_args = function(bufnr) return { '-race', '-count=1' } end
-- bin = function(bufnr) return 'go' end
-- cwd = function(bufnr) return 'your-cwd' end
}),
require("quicktest.adapters.vitest")({
-- bin = function(bufnr) return 'vitest' end
-- cwd = function(bufnr) return bufnr end
-- config_path = function(bufnr) return 'vitest.config.js' end
}),
require("quicktest.adapters.playwright")({
-- bin = function(bufnr) return 'playwright' end
-- cwd = function(bufnr) return bufnr end
-- config_path = function(bufnr) return 'playwright.config.js' end
}),
require("quicktest.adapters.elixir"),
require("quicktest.adapters.criterion"),
require("quicktest.adapters.dart"),
},
-- 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
})
end,
dependencies = {
"nvim-lua/plenary.nvim",
"MunifTanjim/nui.nvim",
-- "m00qek/baleia.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",
},
},
}
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
}),
},
})
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