btv.test
ctx:feed(keys, opts)
t:feed(keys[, opts]) — type vim key-notation, then settle one tick. opts.remap (default true, so the plugin’s own mappings fire), opts.insert (default false), opts.settle (extra ticks to wait, for chained async). Returns self for chaining.
Defined in test.lua.
ctx:cmd(excmd)
t:cmd(excmd) — run an ex-command, then settle a tick.
Defined in test.lua.
ctx:exec(fn)
t:exec(fn) — run fn now (mirrors already fresh) and return its value. fn may
itself await (it runs in the test coroutine).
Defined in test.lua.
ctx:mouse(button, action, row, col, modifier)
t:mouse(button, action, row, col[, modifier]) — send one mouse gesture at a
global SCREEN cell, then settle a tick. The vocabulary is the wire’s: button is
"left" / "right" / "middle" / "wheel" / "move", action is "press" /
"drag" / "release" for a button and "up" / "down" / "left" / "right"
for the wheel, and modifier is "", "s", "c" or "a". Rows and columns are 0-based, as the wire carries them.
The server owns every hit-test — cell to window to buffer position — so this is
the same call a client makes, and a spec drives a click, a drag-select, a
multi-click, a wheel scroll or a divider drag exactly as a user would. Multi-click
detection compares arrival times against 'mousetime', so a double-click is two
press/release pairs in quick succession, not a special action.
t:mouse("left", "press", 4, 12)
t:mouse("left", "release", 4, 12)
btv.test.expect(t:cursor()[1]).to_be(5)
Defined in test.lua.
ctx:wait_for(predicate, opts)
t:wait_for(predicate[, opts]) — await until predicate returns truthy (polling
between ticks), returning that value. Rejects (raising in the test) on timeout.
Use for async UI: a debounced popup, a watch-driven refresh. opts = { tries=,
interval=, message= } (see btv.wait_for).
Defined in test.lua.
ctx:idle()
t:idle() — the pause. A key that is a live prefix of a mapping is WITHHELD
until the next keystroke; the client arms a 'timeoutlen' timer and, on idle,
nudges the server to resolve it. There is no such timer server-side, so a spec
that just waits waits forever — t:idle() is how it says “the user stopped
typing here”, and it is the only way a genuinely-ambiguous mapped prefix
resolves (:set notimeout makes it a no-op, exactly as it does for a client).
t:feed("gg") -- a live prefix of the `ggx` mapping: held
t:idle() -- …and now it resolves to the `gg` built-in
Defined in test.lua.
ctx:sleep(ms)
t:sleep(ms) — await a wall-clock delay (for genuinely time-based behavior).
Defined in test.lua.
ctx:lines(first, last)
No documentation comment in the prelude.
Defined in test.lua.
ctx:line(n)
No documentation comment in the prelude.
Defined in test.lua.
ctx:current_line()
No documentation comment in the prelude.
Defined in test.lua.
ctx:cursor()
No documentation comment in the prelude.
Defined in test.lua.
ctx:mode()
The short mode string (“n”, “i”, “v”, …). btv.mode() returns the
nvim_get_mode table; tests almost always want the code, so unwrap it. Use
t:mode_info() for the full { mode, blocking } table.
Defined in test.lua.
ctx:mode_info()
No documentation comment in the prelude.
Defined in test.lua.
ctx:buf()
No documentation comment in the prelude.
Defined in test.lua.
ctx:keymaps(mode)
No documentation comment in the prelude.
Defined in test.lua.
ctx:float()
t:float() — the content float (which-key popup, hover, …), or nil when closed. { text = joined lines, lines = { raw chunk rows }, title = string|nil }.
Defined in test.lua.
ctx:message()
t:message() — the latest echo / message line (the bottom-of-screen text).
Defined in test.lua.
ctx:showcmd()
t:showcmd() — the 'showcmd' corner: the partly-typed command as the client
would paint it in the last line’s right corner, truncated to vim’s 10 columns.
It is the one view that can see a withheld key run. A mapped prefix
(<Space>f of a <Space>fs map) is held by the keymap matcher and never
reaches the editor, so no buffer, cursor or mode state moves while it waits —
the corner is the only place it exists. The editor’s own pending run (a count,
an armed register, an operator waiting for its motion, a Visual selection’s
size) shows here too.
t:feed("2d")
btv.test.expect(t:showcmd()).to_be("2d")
Empty when nothing is pending — and always empty with 'showcmd' off, which is
what the option means.
Defined in test.lua.
ctx:scroll()
t:scroll() — the scroll-animation gesture the last frame started, as
{ from_row, to_row, duration_ms } (buffer lines, and the milliseconds the
client will take to slide between them), or nil when that frame started none.
A one-shot: only the frame that begins a slide carries it. It describes what
the client is about to animate rather than any state the editor keeps, so no
other view can see it — t:view() reports the destination the scroll already
settled on, animated or not. Always nil under 'noscrollanim' (and at
'scrollanimduration' 0), which is exactly what those options mean.
t:feed("<C-d>")
btv.test.expect(t:scroll().to_row > t:scroll().from_row).to_be(true)
Defined in test.lua.
ctx:matches(row)
t:matches([row]) — the SEARCH-match highlight over the focused window’s rows:
a list of { first, last, current } per row (1-based screen rows, matching
t:screen(); columns are 0-based display cells, end-exclusive). current marks
the one match being walked onto while a / or :s command line is open (vim’s
IncSearch, as against the plain Search on the rest). With row, just that
row’s matches.
The match highlight rides its own wire layer — the server sends column spans and
the client paints the group itself — so no highlight span is ever emitted for it
and t:highlights() cannot see a match at all. This is the only view of
'hlsearch', of 'incsearch', and of the plain pattern preview a half-typed
:s/pat shows before its replacement opens.
t:feed("/needle<CR>")
btv.test.expect(#t:matches(3)).to_be(1)
Defined in test.lua.
ctx:cmdline()
t:cmdline() — the open command line as drawn: the : / / prefix, or an
btv.ui.input / btv.ui.confirm prompt LABEL, followed by the editable text.
nil when no command line is open.
The three are separate on the wire (the client draws the prefix and label itself), so the label — what the prompt actually ASKED — is in none of the other views.
t:feed(":ene")
btv.test.expect(t:cmdline()).to_be(":ene")
Defined in test.lua.
ctx:statusline()
t:statusline() — the rendered status line text, when the mirror carries it.
Defined in test.lua.
ctx:tabline()
t:tabline() — a custom 'tabline' as rendered: the one styled row the
%-format engine produced, with the %#Group# / %nT items already resolved
away. nil when 'tabline' is unset, or when no tabline is drawn this frame.
A wholly different payload from t:tabs(), which reads the STRUCTURED per-region
tab cells the client formats itself. Only one of the two is ever drawn — setting
'tabline' replaces the cells — so a suite for a %!-built tabline can see
nothing in t:tabs() but the built-in labels it was meant to replace.
Defined in test.lua.
ctx:screen()
t:screen() — the focused window’s painted rows, top to bottom, as a list
of strings.
The sibling of t:lines(), and the difference matters: t:lines() is buffer
text, while this is what the client would actually draw. Anything the editor
renders instead of a buffer line is only visible here — a closed fold’s
placeholder, a ~ filler past the end. So a test for 'foldtext' or
'listchars' asserts on this; a test for an edit asserts on t:lines().
Virtual text and virtual lines are not here — they ride their own layers
(a virtual line takes a row, but the row reads blank and its text is in the
layer), which is what t:decor() reads.
t:feed("zM")
btv.test.expect(t:screen()[4]).to_contain("5 lines: fn alpha")
The text is display-scrubbed the same way the wire’s rows are (an unprintable
byte reads as ^X / <xx>), so it is character-for-character what is painted.
Only the focused window; splits and docks are not included.
Defined in test.lua.
ctx:highlights(row)
t:highlights([row]) — the highlight spans painted over the focused window’s
rows: a list of { first, last, group } per row (1-based screen rows, matching
t:screen(); columns are 0-based, end-exclusive display cells). With row, just
that row’s spans.
One of the two views that can see a decoration (t:decor() is the other):
t:lines() is buffer text, t:screen() is the glyphs drawn, and a highlight —
a btv.decor provider’s mark, a btv.decor.expr paint, a treesitter capture —
changes neither. Groups are the names as painted, so a test asserts on the group
it asked for rather than on a colour.
btv.decor.expr([[ local s, e = line:find("TODO") if s then return { { s, e, "Todo" } } end return {} ]])
t:feed("iTODO<Esc>")
btv.test.expect(t:highlights(1)[1][3]).to_be("Todo")
Only the focused window, like t:screen().
Defined in test.lua.
ctx:tabs(region)
t:tabs([region]) — a region’s own tab pages:
{ labels = { "…" }, current = <1-based index> }, or a map of every region when
region is omitted. region is "main" or a dock side ("left" / "right" /
"top" / "bottom").
Tab pages here are PER REGION — the main area and each open dock carry their own
independent set — but nvim_list_tabpages reports one global list whichever
region is focused, so from Lua the stacks are indistinguishable and a spec cannot
tell “a tab was added to the dock” from “a tab was added”.
A region reports the tabline it DRAWS, so a region whose 'showtabline' hides it
(the default 1, with a single tab) reports no labels.
btv.test.expect(#t:tabs("left").labels).to_be(2)
btv.test.expect(#t:tabs("main").labels).to_be(1)
Defined in test.lua.
ctx:view()
t:view() — where the focused window is SCROLLED to:
{ leftcol = <columns>, topline = <buffer line>, numbers = { <line per row> } }.
t:screen() carries each painted row’s full text; the CLIENT is what clips it
to the window and slides it left by leftcol. So sideways scrolling (nowrap)
is visible in no row view at all, and the vertical position had to be guessed
from the text. numbers is the buffer line each painted row shows — the top
line, and which lines are visible at all, since a closed fold takes its rows out
of the list.
t:feed("$")
btv.test.expect(t:view().leftcol > 0).to_be(true)
Defined in test.lua.
ctx:gutter()
t:gutter() — the focused window’s reserved gutter, as
{ number_width = <cells>, sign_width = <cells>, gutter_text_width = <cells>, total = <cells> }.
gutter_text_width is the column gutter_text marks fill, as wide as the widest of
them in the buffer but never wider than the window has left beside its other gutters.
The CLIENT draws the number and sign columns from the widths the server
reserves, so the gutter is in none of the row views — t:screen() is the text
area alone. Without this a spec could read 'numberwidth' / 'signcolumn' back
and nothing more, which reports what was ASKED for rather than what was
reserved: 'numberwidth' is a minimum that grows to fit the largest line
number, 'signcolumn=auto' collapses when no sign is placed, and 'nonumber'
removes the column whatever its width says.
t:cmd("set number numberwidth=4 signcolumn=yes:2")
btv.test.expect(t:gutter().total).to_be(8)
Defined in test.lua.
ctx:menu()
t:menu() — the open float-list menu, or nil when none is up:
{ items = { "…" }, selected = <1-based index|nil> }.
One widget serves four features — the insert-mode completion popup, the :-line
wildmenu, btv.picker, and btv.ui.select — and it is in none of the other
views: not buffer text, not a painted row of the focused window (it floats over
them), and not the content float t:float() reads. Without this a suite for any
of those could only assert on what happened AFTER an accept, never on what was
offered or which row led.
selected is nil while nothing is highlighted — the popup opens noselect, so
<CR> runs the typed line until you actually pick a row. row/col/width/
height describe the box, but in the placement’s own coordinate space (see
below) — to CLICK a row, probe for the cell rather than deriving it.
kinds is the parallel list of per-row kind labels the completion popup
right-aligns ("Snippet", "Function", …), "" for a row that carries none (a
plain buffer word). It is the whole of what the kind column shows, and it is
empty for every other user of the widget.
t:feed(":ene<Tab>")
btv.test.expect(t:menu().items[1]).to_be("enew")
btv.test.expect(t:menu().selected).to_be_nil()
Defined in test.lua.
ctx:rulers()
t:rulers() — the focused window’s 'colorcolumn' rulers, as the 1-based text
columns the client is told to paint with the ColorColumn group.
The third view onto a layer that is not buffer text: t:lines() cannot see a
ruler (it is not text), t:screen() cannot (it is a background behind cells
that may hold nothing), and t:highlights() cannot either — the server sends
the column list and the client draws it, so no span is ever emitted. This is
also the only place the +N rule is visible: a 'textwidth'-relative entry is
accepted but resolves to nothing, while 'colorcolumn' itself still reads
"+1".
t:cmd("set colorcolumn=80,120")
btv.test.expect(t:rulers()).to_equal({ 80, 120 })
Only the focused window, like t:screen().
Defined in test.lua.
ctx:decor(row)
t:decor([row]) — the decoration drawn beside the focused window’s rows:
virtual text, virtual lines, gutter signs, the gutter text column, the full-width
row tint, and the end-of-line diagnostic message. A list of
{ virt_text = "…", virt_pos = "eol", virt_col = 0, virt_lines = "…", sign = "▶",
gutter = " 12 13", line_bg = true, diagnostic = "…", severity = 1 } per row (1-based screen rows,
matching t:screen()); with row, just that row’s. Each key is absent on a row
that carries nothing of that kind.
The companion of t:highlights(), and the split is what each layer is: a
highlight colours the buffer’s own cells, while these draw glyphs that are not in
the buffer at all — a btv.decor.expr badge or sign, a provider’s inline blame, a
diagnostic’s message. t:screen() cannot see them either: it is the buffer’s rows
as painted, and each of these rides its own layer beside them. (Which is also why
diagnostic is its own key rather than part of virt_text: they are separate
layers on the wire, and one row can carry both.)
btv.decor.expr([[ return { { 1, 1, "Todo", virt_text = " <- here", sign_text = ">>" } } ]])
btv.test.expect(t:decor(1).virt_text).to_be(" <- here")
btv.test.expect(t:decor(1).sign).to_be(">>")
virt_text is the row’s placements joined, since one row can carry several;
virt_pos/virt_col describe the first. virt_lines is set on a row that is a
virtual line — a whole extra screen row, so it reads blank in t:screen() and
carries its text here instead. gutter is the row’s gutter_text chunks joined, as
sent: the client pads it to the column’s width (t:gutter().gutter_text_width).
line_bg is true on a row a line_hl_group tints full width. Highlight groups
are absent on purpose: the wire carries a per-frame palette id for these layers, not
a name, so t:highlights() stays the group-level view.
Only the focused window, like t:screen().
Defined in test.lua.