1b279d4d10
e2e Tests / e2e (macos-latest) (push) Waiting to run
e2e Tests / e2e (windows-latest) (push) Waiting to run
Nix CI / check (push) Waiting to run
Python CI / Python bindings (macos-latest) (push) Waiting to run
Python CI / Python bindings (windows-latest) (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
Rust CI / Test (macos-latest) (push) Waiting to run
Rust CI / Test (windows-latest) (push) Waiting to run
Rust CI / Fuzz Tests (macos-latest) (push) Waiting to run
Rust CI / Fuzz Tests (windows-latest) (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Lua CI / lua-language-server type check (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Python CI / Python bindings (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 3s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 3s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 2s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 1s
Rust CI / cargo fmt (push) Failing after 1s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 4s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 1s
Rust CI / cargo clippy (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 1s
Stylua / Check lua files using Stylua (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 4s
e2e Tests / e2e (alpine-musl) (push) Successful in 58m54s
243 lines
7.2 KiB
Lua
243 lines
7.2 KiB
Lua
--- End-to-end snapshot tests for fff.nvim's picker UI.
|
|
local MiniTest = require('mini.test')
|
|
local fixture_lib = require('tests.snapshot.fixture')
|
|
|
|
local PLUGIN_DIR = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':p:h:h')
|
|
local FORCE = vim.env.UPDATE_SNAPSHOTS == '1'
|
|
|
|
local child, fixture
|
|
|
|
local function setup(geometry, opts)
|
|
opts = opts or {}
|
|
fixture = fixture_lib.create()
|
|
child = MiniTest.new_child_neovim()
|
|
child.start({
|
|
'--clean',
|
|
'-n',
|
|
'-i',
|
|
'NONE',
|
|
'--cmd',
|
|
string.format('let &lines = %d', geometry.rows),
|
|
'--cmd',
|
|
string.format('let &columns = %d', geometry.cols),
|
|
}, {
|
|
connection_timeout = 15000,
|
|
})
|
|
|
|
child.o.lines = geometry.rows
|
|
child.o.columns = geometry.cols
|
|
|
|
local debug_enabled = opts.debug == true
|
|
-- Default show_file_info hides timings: Modified/Accessed timestamps drift
|
|
-- between runs and would otherwise force `ignore_text` on those rows. Tests
|
|
-- can override (or restore) by passing `opts.show_file_info`.
|
|
local show_file_info = opts.show_file_info
|
|
or { file_info = true, score_breakdown = true, timings = false, full_path = true }
|
|
|
|
child.lua(
|
|
string.format(
|
|
[[
|
|
local plugin = %q
|
|
vim.opt.runtimepath:prepend(plugin)
|
|
package.path = plugin .. '/lua/?.lua;' .. plugin .. '/lua/?/init.lua;' .. package.path
|
|
vim.cmd('cd ' .. vim.fn.fnameescape(%q))
|
|
vim.cmd('syntax off')
|
|
local winborder = %q
|
|
if winborder ~= '' then vim.o.winborder = winborder end
|
|
vim.g.fff = {
|
|
prompt = '> ',
|
|
frecency = { enabled = true, db_path = %q },
|
|
history = { enabled = true, db_path = %q },
|
|
logging = { enabled = false },
|
|
debug = {
|
|
enabled = %s,
|
|
show_scores = %s,
|
|
show_file_info = %s,
|
|
},
|
|
}
|
|
require('fff.core').ensure_initialized()
|
|
require('fff.rust').wait_for_initial_scan(8000)
|
|
]],
|
|
PLUGIN_DIR,
|
|
fixture.root,
|
|
geometry.winborder or '',
|
|
fixture.frecency_db,
|
|
fixture.history_db,
|
|
tostring(debug_enabled),
|
|
tostring(debug_enabled),
|
|
vim.inspect(show_file_info)
|
|
)
|
|
)
|
|
end
|
|
|
|
local function teardown()
|
|
if child and child.is_running() then pcall(child.stop) end
|
|
if fixture then fixture_lib.cleanup(fixture) end
|
|
child, fixture = nil, nil
|
|
end
|
|
|
|
--- @param opts table|nil { ignore_text?: number[] }
|
|
local function assert_snapshot_match(opts)
|
|
opts = opts or {}
|
|
MiniTest.expect.reference_screenshot(child.get_screenshot(), nil, {
|
|
force = FORCE,
|
|
ignore_text = opts.ignore_text or false,
|
|
})
|
|
end
|
|
|
|
local function open_picker(prompt_position, query)
|
|
child.lua(
|
|
string.format('require("fff.picker_ui.picker_ui").open({ layout = { prompt_position = %q } })', prompt_position)
|
|
)
|
|
vim.loop.sleep(400)
|
|
|
|
if query and query ~= '' then
|
|
child.type_keys(query)
|
|
vim.loop.sleep(400)
|
|
end
|
|
end
|
|
|
|
local LAYOUTS = {
|
|
{ name = 'wide', cols = 180, rows = 40, winborder = 'double' },
|
|
{ name = 'default', cols = 140, rows = 32 }, -- standard on most screens
|
|
{ name = 'narrow', cols = 70, rows = 24, winborder = 'rounded' },
|
|
-- Extra-wide: exists primarily so the file_info panel hits the H2 layout.
|
|
{ name = 'xwide', cols = 240, rows = 48 },
|
|
}
|
|
|
|
local T = MiniTest.new_set()
|
|
|
|
local PROMPT_POSITIONS = { 'bottom', 'top' }
|
|
|
|
for _, geometry in ipairs(LAYOUTS) do
|
|
local set = MiniTest.new_set({
|
|
hooks = {
|
|
pre_case = function() setup(geometry) end,
|
|
post_case = teardown,
|
|
},
|
|
})
|
|
|
|
-- Run every per-geometry case for both prompt positions: layout math and
|
|
-- list rendering diverge between top/bottom (see AGENTS.md), so a snapshot
|
|
-- on a single side would silently miss regressions in the other.
|
|
for _, prompt in ipairs(PROMPT_POSITIONS) do
|
|
set['empty_' .. prompt] = function()
|
|
open_picker(prompt)
|
|
assert_snapshot_match()
|
|
end
|
|
|
|
set['query_main_' .. prompt] = function()
|
|
open_picker(prompt, 'main')
|
|
assert_snapshot_match()
|
|
end
|
|
|
|
set['no_results_' .. prompt] = function()
|
|
open_picker(prompt, 'zzzzzzzzz')
|
|
assert_snapshot_match()
|
|
end
|
|
|
|
set['cursor_second_item_' .. prompt] = function()
|
|
open_picker(prompt)
|
|
-- Bottom prompt visually goes up with <Down>; top prompt goes down with <Down>.
|
|
-- Either way one keypress moves to the second item — what we want to capture.
|
|
child.type_keys('<Down>')
|
|
vim.loop.sleep(200)
|
|
assert_snapshot_match()
|
|
end
|
|
end
|
|
|
|
T[geometry.name] = set
|
|
end
|
|
|
|
-- File info panel snapshots. Timings are disabled at the config level (see
|
|
-- `setup`) so the snapshots stay deterministic — Modified/Accessed timestamps
|
|
-- drift between runs. We snapshot a narrow and a wide variant for both prompt
|
|
-- positions to keep the adaptive panel layout (label widths, section headers,
|
|
-- top vs bottom prompt geometry) covered.
|
|
local debug_narrow_set = MiniTest.new_set({
|
|
hooks = {
|
|
pre_case = function() setup(LAYOUTS[2], { debug = true }) end, -- default 140x32, panel ~57 cols
|
|
post_case = teardown,
|
|
},
|
|
})
|
|
|
|
for _, prompt in ipairs(PROMPT_POSITIONS) do
|
|
debug_narrow_set['file_info_panel_' .. prompt] = function()
|
|
open_picker(prompt, 'main')
|
|
assert_snapshot_match()
|
|
end
|
|
end
|
|
T['debug_narrow'] = debug_narrow_set
|
|
|
|
local debug_wide_set = MiniTest.new_set({
|
|
hooks = {
|
|
pre_case = function() setup(LAYOUTS[4], { debug = true }) end, -- xwide 240x48, panel ~96 cols
|
|
post_case = teardown,
|
|
},
|
|
})
|
|
|
|
for _, prompt in ipairs(PROMPT_POSITIONS) do
|
|
debug_wide_set['file_info_panel_' .. prompt] = function()
|
|
open_picker(prompt, 'main')
|
|
assert_snapshot_match()
|
|
end
|
|
end
|
|
T['debug_wide'] = debug_wide_set
|
|
|
|
T['combo'] = MiniTest.new_set({
|
|
hooks = {
|
|
pre_case = function() setup({ cols = 140, rows = 32 }) end,
|
|
post_case = teardown,
|
|
},
|
|
})
|
|
|
|
local function train_combo()
|
|
-- Train: "main" → src/main.rs four times so open_count crosses the default
|
|
-- min_combo_count (3). track_query_completion is async; pause between calls
|
|
-- + final settle for the lmdb writer.
|
|
for _ = 1, 4 do
|
|
child.lua(
|
|
string.format('require("fff.rust").track_query_completion(%q, %q)', 'main', fixture.root .. '/src/main.rs')
|
|
)
|
|
vim.loop.sleep(120)
|
|
end
|
|
vim.loop.sleep(400)
|
|
end
|
|
|
|
for _, prompt in ipairs(PROMPT_POSITIONS) do
|
|
T['combo']['boost_' .. prompt] = function()
|
|
train_combo()
|
|
open_picker(prompt, 'main')
|
|
-- Combo overlay float renders asynchronously after render_list.
|
|
vim.loop.sleep(400)
|
|
assert_snapshot_match()
|
|
end
|
|
end
|
|
|
|
T['scrollbar'] = MiniTest.new_set({
|
|
hooks = {
|
|
pre_case = function() setup({ cols = 140, rows = 32 }) end,
|
|
post_case = teardown,
|
|
},
|
|
})
|
|
|
|
-- Cursor advance key differs by prompt position: bottom prompt iterates the
|
|
-- list in reverse so `<Up>` walks toward higher indices; top prompt is
|
|
-- conventional. Either way we walk past the last in-page item to trigger
|
|
-- load_next_page and surface the scrollbar thumb at the new offset.
|
|
local SCROLL_KEY = { bottom = '<Up>', top = '<Down>' }
|
|
|
|
for _, prompt in ipairs(PROMPT_POSITIONS) do
|
|
T['scrollbar']['next_page_' .. prompt] = function()
|
|
open_picker(prompt)
|
|
for _ = 1, 30 do
|
|
child.type_keys(SCROLL_KEY[prompt])
|
|
vim.loop.sleep(20)
|
|
end
|
|
vim.loop.sleep(400)
|
|
assert_snapshot_match()
|
|
end
|
|
end
|
|
|
|
return T
|