mirror of
https://github.com/chenasraf/input-form.nvim.git
synced 2026-05-17 17:38:01 +00:00
feat: input validators
This commit is contained in:
86
README.md
86
README.md
@@ -139,6 +139,92 @@ All inputs share `name` (string, required — the key in the result table) and
|
||||
|
||||
`value()` returns the selected `id` (not the label).
|
||||
|
||||
## Validation
|
||||
|
||||
Each input spec accepts an optional `validator` function:
|
||||
|
||||
```lua
|
||||
validator = fun(value: any): string|nil
|
||||
```
|
||||
|
||||
Return a non-empty error message string to mark the input invalid, or `nil` /
|
||||
`""` when valid. The error message is shown in the input's bottom border
|
||||
(red), and the border + label turn red too. Validation runs:
|
||||
|
||||
- **On blur** — the first time the user leaves the field it is marked
|
||||
"touched" and the validator runs. Nothing is shown before that.
|
||||
- **On change** — once touched, each buffer change re-runs the validator.
|
||||
- **On submit** — `form:submit()` force-validates every input (touched or
|
||||
not). If any input has an error, submission is blocked, all errors are
|
||||
rendered, and focus moves to the first invalid input.
|
||||
|
||||
### Built-in validators
|
||||
|
||||
```lua
|
||||
local V = require("input-form").validators
|
||||
|
||||
V.non_empty([msg]) -- require a non-empty value
|
||||
V.min_length(n, [msg]) -- at least `n` characters
|
||||
V.max_length(n, [msg]) -- at most `n` characters
|
||||
V.matches(lua_pattern, [msg]) -- match a Lua pattern
|
||||
V.is_number([msg]) -- tonumber() must succeed
|
||||
V.one_of({ "a", "b", ... }, [msg]) -- value must be in the list
|
||||
V.custom(predicate, msg) -- wrap a `fun(v): boolean` predicate
|
||||
V.chain(v1, v2, ...) -- run validators in order, first error wins
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
local f = require("input-form")
|
||||
local V = f.validators
|
||||
|
||||
f.create_form({
|
||||
inputs = {
|
||||
{
|
||||
name = "id",
|
||||
label = "Enter ID",
|
||||
type = "text",
|
||||
validator = V.chain(
|
||||
V.non_empty(),
|
||||
V.min_length(3),
|
||||
V.matches("^[%w_-]+$", "Only letters, digits, - and _")
|
||||
),
|
||||
},
|
||||
{
|
||||
name = "age",
|
||||
label = "Age",
|
||||
type = "text",
|
||||
validator = V.chain(V.non_empty(), V.is_number()),
|
||||
},
|
||||
},
|
||||
on_submit = function(results)
|
||||
vim.print(results) -- only runs if every validator passes
|
||||
end,
|
||||
}):show()
|
||||
```
|
||||
|
||||
Custom validators are just functions — no need to use the builder helpers if
|
||||
you'd rather write one inline:
|
||||
|
||||
```lua
|
||||
validator = function(value)
|
||||
if value == "admin" then
|
||||
return "Username 'admin' is reserved"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Highlight groups
|
||||
|
||||
Error rendering uses three highlight groups. Override them to re-theme:
|
||||
|
||||
```lua
|
||||
vim.api.nvim_set_hl(0, "InputFormFieldError", { fg = "#ff5555" })
|
||||
vim.api.nvim_set_hl(0, "InputFormFieldErrorBorder", { fg = "#ff5555" })
|
||||
vim.api.nvim_set_hl(0, "InputFormFieldErrorTitle", { fg = "#ff5555", bold = true })
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Defaults:
|
||||
|
||||
@@ -69,6 +69,11 @@ Expose the Form class for advanced use.
|
||||
`M.config`
|
||||
Expose the config module.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.validators*
|
||||
`M.validators`
|
||||
Expose the built-in validator library. See |input-form.validators|.
|
||||
|
||||
|
||||
==============================================================================
|
||||
------------------------------------------------------------------------------
|
||||
@@ -137,4 +142,98 @@ Return ~
|
||||
`(table)`
|
||||
|
||||
|
||||
==============================================================================
|
||||
------------------------------------------------------------------------------
|
||||
*input-form.validators*
|
||||
Validators for form inputs.
|
||||
|
||||
A validator is a function `fun(value): string|nil`. It receives the
|
||||
current input value and returns a non-empty error message string when
|
||||
invalid, or `nil` / `""` when valid.
|
||||
|
||||
This module exposes factory functions for common validators plus a
|
||||
`chain` combinator that runs several validators in order and returns the
|
||||
first error.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.non_empty()*
|
||||
`M.non_empty`({msg})
|
||||
Require the field to have a non-empty value.
|
||||
Parameters ~
|
||||
{msg} `(string|nil)` Override error message.
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.min_length()*
|
||||
`M.min_length`({n}, {msg})
|
||||
Require the value's length to be at least `n` characters.
|
||||
Parameters ~
|
||||
{n} `(integer)`
|
||||
{msg} `(string|nil)`
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.max_length()*
|
||||
`M.max_length`({n}, {msg})
|
||||
Require the value's length to be at most `n` characters.
|
||||
Parameters ~
|
||||
{n} `(integer)`
|
||||
{msg} `(string|nil)`
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.matches()*
|
||||
`M.matches`({pattern}, {msg})
|
||||
Require the value to match a Lua pattern.
|
||||
Parameters ~
|
||||
{pattern} `(string)` Lua pattern (not PCRE).
|
||||
{msg} `(string|nil)`
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.is_number()*
|
||||
`M.is_number`({msg})
|
||||
Require the value to parse as a number.
|
||||
Parameters ~
|
||||
{msg} `(string|nil)`
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.one_of()*
|
||||
`M.one_of`({choices}, {msg})
|
||||
Require the value to be one of the given choices (useful for text inputs
|
||||
that must match a fixed allowlist; select inputs should use their
|
||||
`options` list instead).
|
||||
Parameters ~
|
||||
{choices} `(table)` List of allowed values.
|
||||
{msg} `(string|nil)`
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.custom()*
|
||||
`M.custom`({predicate}, {msg})
|
||||
Wrap a predicate `fun(value): boolean` as a validator.
|
||||
Parameters ~
|
||||
{predicate} `(function)`
|
||||
{msg} `(string)` Error message to return when the predicate is false.
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
*M.chain()*
|
||||
`M.chain`({...})
|
||||
Combine multiple validators. Runs them in order and returns the first
|
||||
non-empty error. Accepts either a list table or a varargs list.
|
||||
Parameters ~
|
||||
{...} `(function|table)`
|
||||
Return ~
|
||||
`(function)`
|
||||
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||
10
doc/tags
10
doc/tags
@@ -1,10 +1,20 @@
|
||||
Default input-form.txt /*Default*
|
||||
M.Form input-form.txt /*M.Form*
|
||||
M.chain() input-form.txt /*M.chain()*
|
||||
M.config input-form.txt /*M.config*
|
||||
M.custom() input-form.txt /*M.custom()*
|
||||
M.is_number() input-form.txt /*M.is_number()*
|
||||
M.matches() input-form.txt /*M.matches()*
|
||||
M.max_length() input-form.txt /*M.max_length()*
|
||||
M.min_length() input-form.txt /*M.min_length()*
|
||||
M.non_empty() input-form.txt /*M.non_empty()*
|
||||
M.one_of() input-form.txt /*M.one_of()*
|
||||
M.validators input-form.txt /*M.validators*
|
||||
input-form.config input-form.txt /*input-form.config*
|
||||
input-form.config.setup input-form.txt /*input-form.config.setup*
|
||||
input-form.create_form input-form.txt /*input-form.create_form*
|
||||
input-form.nvim input-form.txt /*input-form.nvim*
|
||||
input-form.setup input-form.txt /*input-form.setup*
|
||||
input-form.validators input-form.txt /*input-form.validators*
|
||||
register_helptags() input-form.txt /*register_helptags()*
|
||||
values: input-form.txt /*values:*
|
||||
|
||||
@@ -235,8 +235,16 @@ function M:results()
|
||||
return out
|
||||
end
|
||||
|
||||
--- Submit the form: gather values, close windows, invoke `on_submit(results)`.
|
||||
--- Submit the form: gather values, run validators, and invoke
|
||||
--- `on_submit(results)` only if everything validates. If any input has an
|
||||
--- error, submission is blocked, all inputs are force-validated (so the user
|
||||
--- sees every error, including ones on untouched fields), and focus moves to
|
||||
--- the first invalid input.
|
||||
function M:submit()
|
||||
if self._visible and self:_validate_all() then
|
||||
-- Validation failed — do not close, do not invoke on_submit.
|
||||
return
|
||||
end
|
||||
local results = self:results()
|
||||
self:hide()
|
||||
if self._on_submit then
|
||||
@@ -244,6 +252,34 @@ function M:submit()
|
||||
end
|
||||
end
|
||||
|
||||
--- Run every input's validator (marking each as touched first) and render
|
||||
--- results. Returns `true` if any input has an error.
|
||||
function M:_validate_all()
|
||||
local any_error = false
|
||||
local first_bad = nil
|
||||
for i, input in ipairs(self._inputs) do
|
||||
if input.validator then
|
||||
input._touched = true
|
||||
local err = input.validator(input:value())
|
||||
if err == "" then
|
||||
err = nil
|
||||
end
|
||||
input._error = err
|
||||
self:_render_validation(input)
|
||||
if err and not first_bad then
|
||||
first_bad = i
|
||||
end
|
||||
if err then
|
||||
any_error = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if first_bad then
|
||||
self:_focus(first_bad)
|
||||
end
|
||||
return any_error
|
||||
end
|
||||
|
||||
--- Cancel the form: close windows, invoke `on_cancel()` if provided.
|
||||
function M:cancel()
|
||||
self:hide()
|
||||
@@ -252,6 +288,112 @@ function M:cancel()
|
||||
end
|
||||
end
|
||||
|
||||
--- Install validation autocmds for an input. No-op if the input has no
|
||||
--- validator. Validation runs:
|
||||
--- - on `WinLeave` (blurring the field marks it touched and validates)
|
||||
--- - on `TextChanged` / `TextChangedI` IF the input is already touched
|
||||
---
|
||||
--- For `select` inputs, the user "touches" the field by picking an option;
|
||||
--- `_render_display`'s `nvim_buf_set_lines` call also fires `TextChanged`
|
||||
--- so the same path handles re-validation there.
|
||||
function M:_install_validation(input)
|
||||
if not input.validator then
|
||||
return
|
||||
end
|
||||
local buf = input.buf
|
||||
if not buf then
|
||||
return
|
||||
end
|
||||
local form = self
|
||||
local group = vim.api.nvim_create_augroup("InputFormValidate_" .. tostring(buf), { clear = true })
|
||||
input._val_group = group
|
||||
|
||||
vim.api.nvim_create_autocmd("WinLeave", {
|
||||
group = group,
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
input._touched = true
|
||||
form:_validate_input(input)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
|
||||
group = group,
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
if input._touched then
|
||||
form:_validate_input(input)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- For `select` inputs, changes happen while the dropdown buffer is current,
|
||||
-- so `TextChanged` on the display buffer doesn't fire. The input exposes an
|
||||
-- `_on_change` hook that we use to mark it touched and re-validate.
|
||||
input._on_change = function()
|
||||
input._touched = true
|
||||
form:_validate_input(input)
|
||||
end
|
||||
end
|
||||
|
||||
--- Run the validator for one input and update its visual error state.
|
||||
function M:_validate_input(input)
|
||||
if not input.validator then
|
||||
return
|
||||
end
|
||||
local err = input.validator(input:value())
|
||||
if err == "" then
|
||||
err = nil
|
||||
end
|
||||
input._error = err
|
||||
self:_render_validation(input)
|
||||
end
|
||||
|
||||
--- Apply/clear the red border, title, and footer error message on an input's
|
||||
--- floating window based on its current `_error` state.
|
||||
function M:_render_validation(input)
|
||||
local win = input.win
|
||||
if not (win and vim.api.nvim_win_is_valid(win)) then
|
||||
return
|
||||
end
|
||||
local has_error = input._error ~= nil
|
||||
|
||||
if has_error then
|
||||
vim.wo[win].winhl = table.concat({
|
||||
"NormalFloat:InputFormField",
|
||||
"FloatBorder:InputFormFieldErrorBorder",
|
||||
"FloatTitle:InputFormFieldErrorTitle",
|
||||
"FloatFooter:InputFormFieldError",
|
||||
}, ",")
|
||||
else
|
||||
vim.wo[win].winhl = table.concat({
|
||||
"NormalFloat:InputFormField",
|
||||
"FloatBorder:InputFormFieldBorder",
|
||||
"FloatTitle:InputFormFieldTitle",
|
||||
}, ",")
|
||||
end
|
||||
|
||||
-- Footer (error message) requires nvim 0.10+.
|
||||
if vim.fn.has("nvim-0.10") == 1 then
|
||||
local ok, cfg = pcall(vim.api.nvim_win_get_config, win)
|
||||
if ok and cfg and cfg.relative ~= "" then
|
||||
if has_error then
|
||||
-- Truncate to window width with an ellipsis.
|
||||
local max_w = (cfg.width or 0) - 2
|
||||
local msg = input._error
|
||||
if max_w > 3 and vim.fn.strdisplaywidth(msg) > max_w then
|
||||
msg = vim.fn.strcharpart(msg, 0, max_w - 1) .. "…"
|
||||
end
|
||||
cfg.footer = " " .. msg .. " "
|
||||
cfg.footer_pos = "left"
|
||||
else
|
||||
cfg.footer = ""
|
||||
end
|
||||
pcall(vim.api.nvim_win_set_config, win, cfg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Build a help-line string describing the active keymaps.
|
||||
function M:_help_line()
|
||||
local km = config.options.keymaps
|
||||
|
||||
@@ -74,6 +74,9 @@ M.Form = Form
|
||||
--- Expose the config module.
|
||||
M.config = config
|
||||
|
||||
--- Expose the built-in validator library. See |input-form.validators|.
|
||||
M.validators = require("input-form.validators")
|
||||
|
||||
-- Run once on first require so users/plugin devs don't have to call `setup()`
|
||||
-- just to get working help tags.
|
||||
register_helptags()
|
||||
|
||||
@@ -17,7 +17,11 @@ function M.build(spec)
|
||||
local t = spec.type or "text"
|
||||
local impl = M.types[t]
|
||||
assert(impl, "unknown input type: " .. tostring(t))
|
||||
return impl.new(spec)
|
||||
local input = impl.new(spec)
|
||||
input.validator = spec.validator
|
||||
input._touched = false
|
||||
input._error = nil
|
||||
return input
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -189,9 +189,14 @@ function M:open_dropdown()
|
||||
end
|
||||
local function confirm()
|
||||
local row = vim.api.nvim_win_get_cursor(self.dropdown_win)[1]
|
||||
self._selected_id = self.options[row].id
|
||||
local new_id = self.options[row].id
|
||||
local changed = new_id ~= self._selected_id
|
||||
self._selected_id = new_id
|
||||
self:_render_display()
|
||||
self:close_dropdown()
|
||||
if changed and self._on_change then
|
||||
self._on_change()
|
||||
end
|
||||
end
|
||||
map("<CR>", confirm)
|
||||
map("<Esc>", function()
|
||||
@@ -220,10 +225,14 @@ end
|
||||
|
||||
--- Programmatically set the selected option by id (used in tests & external callers).
|
||||
function M:select_id(id)
|
||||
local changed = id ~= self._selected_id
|
||||
for _, opt in ipairs(self.options) do
|
||||
if opt.id == id then
|
||||
self._selected_id = id
|
||||
self:_render_display()
|
||||
if changed and self._on_change then
|
||||
self._on_change()
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
139
lua/input-form/validators.lua
Normal file
139
lua/input-form/validators.lua
Normal file
@@ -0,0 +1,139 @@
|
||||
--- Validators for form inputs.
|
||||
---
|
||||
--- A validator is a function `fun(value): string|nil`. It receives the
|
||||
--- current input value and returns a non-empty error message string when
|
||||
--- invalid, or `nil` / `""` when valid.
|
||||
---
|
||||
--- This module exposes factory functions for common validators plus a
|
||||
--- `chain` combinator that runs several validators in order and returns the
|
||||
--- first error.
|
||||
---
|
||||
---@tag input-form.validators
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Require the field to have a non-empty value.
|
||||
---@param msg string|nil Override error message.
|
||||
---@return function
|
||||
function M.non_empty(msg)
|
||||
msg = msg or "This field is required"
|
||||
return function(value)
|
||||
if value == nil then
|
||||
return msg
|
||||
end
|
||||
if type(value) == "string" and value == "" then
|
||||
return msg
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Require the value's length to be at least `n` characters.
|
||||
---@param n integer
|
||||
---@param msg string|nil
|
||||
---@return function
|
||||
function M.min_length(n, msg)
|
||||
return function(value)
|
||||
local s = type(value) == "string" and value or tostring(value or "")
|
||||
if vim.fn.strchars(s) < n then
|
||||
return msg or ("Must be at least " .. n .. " characters")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Require the value's length to be at most `n` characters.
|
||||
---@param n integer
|
||||
---@param msg string|nil
|
||||
---@return function
|
||||
function M.max_length(n, msg)
|
||||
return function(value)
|
||||
local s = type(value) == "string" and value or tostring(value or "")
|
||||
if vim.fn.strchars(s) > n then
|
||||
return msg or ("Must be at most " .. n .. " characters")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Require the value to match a Lua pattern.
|
||||
---@param pattern string Lua pattern (not PCRE).
|
||||
---@param msg string|nil
|
||||
---@return function
|
||||
function M.matches(pattern, msg)
|
||||
return function(value)
|
||||
local s = type(value) == "string" and value or tostring(value or "")
|
||||
if not s:match(pattern) then
|
||||
return msg or "Invalid format"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Require the value to parse as a number.
|
||||
---@param msg string|nil
|
||||
---@return function
|
||||
function M.is_number(msg)
|
||||
return function(value)
|
||||
if value == nil or value == "" or tonumber(value) == nil then
|
||||
return msg or "Must be a number"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Require the value to be one of the given choices (useful for text inputs
|
||||
--- that must match a fixed allowlist; select inputs should use their
|
||||
--- `options` list instead).
|
||||
---@param choices table List of allowed values.
|
||||
---@param msg string|nil
|
||||
---@return function
|
||||
function M.one_of(choices, msg)
|
||||
return function(value)
|
||||
for _, c in ipairs(choices) do
|
||||
if c == value then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return msg or "Value is not allowed"
|
||||
end
|
||||
end
|
||||
|
||||
--- Wrap a predicate `fun(value): boolean` as a validator.
|
||||
---@param predicate function
|
||||
---@param msg string Error message to return when the predicate is false.
|
||||
---@return function
|
||||
function M.custom(predicate, msg)
|
||||
return function(value)
|
||||
if predicate(value) then
|
||||
return nil
|
||||
end
|
||||
return msg
|
||||
end
|
||||
end
|
||||
|
||||
--- Combine multiple validators. Runs them in order and returns the first
|
||||
--- non-empty error. Accepts either a list table or a varargs list.
|
||||
---@param ... function|table
|
||||
---@return function
|
||||
function M.chain(...)
|
||||
local validators = { ... }
|
||||
if
|
||||
#validators == 1
|
||||
and type(validators[1]) == "table"
|
||||
and type(validators[1][1]) == "function"
|
||||
then
|
||||
validators = validators[1]
|
||||
end
|
||||
return function(value)
|
||||
for _, v in ipairs(validators) do
|
||||
local err = v(value)
|
||||
if err and err ~= "" then
|
||||
return err
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -5,4 +5,5 @@ require("mini.doc").setup()
|
||||
MiniDoc.generate({
|
||||
"lua/input-form/init.lua",
|
||||
"lua/input-form/config.lua",
|
||||
"lua/input-form/validators.lua",
|
||||
}, "doc/input-form.txt")
|
||||
|
||||
@@ -187,6 +187,81 @@ T["form"]["multiline inputs do not rebind <CR>"] = function()
|
||||
eq(has_cr, false)
|
||||
end
|
||||
|
||||
T["form"]["validation"] = MiniTest.new_set()
|
||||
|
||||
local function make_validated_form(child)
|
||||
child.lua([[
|
||||
local V = require('input-form.validators')
|
||||
_G.submit_result = nil
|
||||
_G.vf = require('input-form').create_form({
|
||||
inputs = {
|
||||
{ name = 'id', label = 'Enter ID', type = 'text',
|
||||
default = '',
|
||||
validator = V.chain(V.non_empty(), V.min_length(3)) },
|
||||
{ name = 'body', label = 'Body', type = 'multiline', default = '' },
|
||||
},
|
||||
on_submit = function(r) _G.submit_result = r end,
|
||||
})
|
||||
_G.vf:show()
|
||||
]])
|
||||
end
|
||||
|
||||
T["form"]["validation"]["no error shown before the field is touched"] = function()
|
||||
make_validated_form(child)
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._touched]]), false)
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._error]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["form"]["validation"]["blur marks touched and runs validator"] = function()
|
||||
make_validated_form(child)
|
||||
-- Move focus from input 1 to input 2 — fires WinLeave on input 1.
|
||||
child.lua([[_G.vf:focus_next()]])
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._touched]]), true)
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._error]]), "This field is required")
|
||||
end
|
||||
|
||||
T["form"]["validation"]["re-validates on change after touched"] = function()
|
||||
make_validated_form(child)
|
||||
child.lua([[_G.vf:focus_next()]]) -- blur input 1, errors
|
||||
child.lua([[_G.vf:focus_prev()]]) -- back to input 1
|
||||
child.lua([[vim.api.nvim_buf_set_lines(_G.vf._inputs[1].buf, 0, -1, false, { 'ab' })]])
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._error]]), "Must be at least 3 characters")
|
||||
child.lua([[vim.api.nvim_buf_set_lines(_G.vf._inputs[1].buf, 0, -1, false, { 'abcd' })]])
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._error]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["form"]["validation"]["submit blocked when any input is invalid"] = function()
|
||||
make_validated_form(child)
|
||||
child.lua([[_G.vf:submit()]])
|
||||
-- submit should NOT have run on_submit
|
||||
eq(child.lua_get([[_G.submit_result]]), vim.NIL)
|
||||
-- form still visible
|
||||
eq(child.lua_get([[_G.vf._visible]]), true)
|
||||
-- first input is now touched and errored
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._touched]]), true)
|
||||
eq(child.lua_get([[_G.vf._inputs[1]._error]]), "This field is required")
|
||||
-- focus moved to the first invalid input
|
||||
eq(child.lua_get([[_G.vf._focus_idx]]), 1)
|
||||
end
|
||||
|
||||
T["form"]["validation"]["submit proceeds once all inputs are valid"] = function()
|
||||
make_validated_form(child)
|
||||
child.lua([[vim.api.nvim_buf_set_lines(_G.vf._inputs[1].buf, 0, -1, false, { 'valid id' })]])
|
||||
child.lua([[_G.vf:submit()]])
|
||||
eq(child.lua_get([[type(_G.submit_result)]]), "table")
|
||||
eq(child.lua_get([[_G.submit_result.id]]), "valid id")
|
||||
eq(child.lua_get([[_G.vf._visible]]), false)
|
||||
end
|
||||
|
||||
T["form"]["validation"]["inputs without a validator are never marked touched"] = function()
|
||||
make_validated_form(child)
|
||||
-- Second input has no validator.
|
||||
child.lua([[_G.vf:focus_next()]])
|
||||
child.lua([[_G.vf:focus_prev()]])
|
||||
eq(child.lua_get([[_G.vf._inputs[2]._touched]]), false)
|
||||
eq(child.lua_get([[_G.vf._inputs[2]._error]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["form"]["keymaps are installed on each input buffer"] = function()
|
||||
child.lua([[_G.f = _G.make_form(); _G.f:show()]])
|
||||
-- <Tab> should be mapped in normal mode on the first input's buffer.
|
||||
|
||||
89
tests/test_validators.lua
Normal file
89
tests/test_validators.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
local helpers = dofile("tests/helpers.lua")
|
||||
local MiniTest = require("mini.test")
|
||||
|
||||
local child = helpers.new_child_neovim()
|
||||
local eq = helpers.expect.equality
|
||||
|
||||
local T = MiniTest.new_set({
|
||||
hooks = {
|
||||
pre_case = function()
|
||||
child.restart({ "-u", "scripts/minimal_init.lua" })
|
||||
child.lua([[V = require('input-form.validators')]])
|
||||
end,
|
||||
post_once = child.stop,
|
||||
},
|
||||
})
|
||||
|
||||
T["validators"] = MiniTest.new_set()
|
||||
|
||||
T["validators"]["non_empty"] = function()
|
||||
eq(child.lua_get([[V.non_empty()("")]]), "This field is required")
|
||||
eq(child.lua_get([[V.non_empty()(nil)]]), "This field is required")
|
||||
eq(child.lua_get([[V.non_empty()("ok")]]), vim.NIL)
|
||||
eq(child.lua_get([[V.non_empty("nope")("")]]), "nope")
|
||||
end
|
||||
|
||||
T["validators"]["min_length"] = function()
|
||||
eq(child.lua_get([[V.min_length(3)("ab")]]), "Must be at least 3 characters")
|
||||
eq(child.lua_get([[V.min_length(3)("abc")]]), vim.NIL)
|
||||
eq(child.lua_get([[V.min_length(3)("abcd")]]), vim.NIL)
|
||||
eq(child.lua_get([[V.min_length(3, "too short")("a")]]), "too short")
|
||||
end
|
||||
|
||||
T["validators"]["max_length"] = function()
|
||||
eq(child.lua_get([[V.max_length(3)("abcd")]]), "Must be at most 3 characters")
|
||||
eq(child.lua_get([[V.max_length(3)("abc")]]), vim.NIL)
|
||||
eq(child.lua_get([[V.max_length(3)("")]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["validators"]["matches"] = function()
|
||||
eq(child.lua_get([[V.matches('^%d+$')("abc")]]), "Invalid format")
|
||||
eq(child.lua_get([[V.matches('^%d+$')("123")]]), vim.NIL)
|
||||
eq(child.lua_get([[V.matches('^%d+$', 'digits only')("x")]]), "digits only")
|
||||
end
|
||||
|
||||
T["validators"]["is_number"] = function()
|
||||
eq(child.lua_get([[V.is_number()("abc")]]), "Must be a number")
|
||||
eq(child.lua_get([[V.is_number()("")]]), "Must be a number")
|
||||
eq(child.lua_get([[V.is_number()("42")]]), vim.NIL)
|
||||
eq(child.lua_get([[V.is_number()("3.14")]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["validators"]["one_of"] = function()
|
||||
eq(child.lua_get([[V.one_of({ 'a', 'b' })('c')]]), "Value is not allowed")
|
||||
eq(child.lua_get([[V.one_of({ 'a', 'b' })('a')]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["validators"]["custom"] = function()
|
||||
eq(child.lua_get([[V.custom(function(v) return v == 'yes' end, 'say yes')('no')]]), "say yes")
|
||||
eq(child.lua_get([[V.custom(function(v) return v == 'yes' end, 'say yes')('yes')]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["validators"]["chain returns first error (varargs)"] = function()
|
||||
eq(child.lua_get([[V.chain(V.non_empty(), V.min_length(3))("")]]), "This field is required")
|
||||
eq(
|
||||
child.lua_get([[V.chain(V.non_empty(), V.min_length(3))("ab")]]),
|
||||
"Must be at least 3 characters"
|
||||
)
|
||||
eq(child.lua_get([[V.chain(V.non_empty(), V.min_length(3))("abcd")]]), vim.NIL)
|
||||
end
|
||||
|
||||
T["validators"]["chain accepts a list"] = function()
|
||||
eq(
|
||||
child.lua_get([[V.chain({ V.non_empty(), V.min_length(3) })("ab")]]),
|
||||
"Must be at least 3 characters"
|
||||
)
|
||||
end
|
||||
|
||||
T["validators"]["chain treats empty string return as success"] = function()
|
||||
-- A validator that returns "" should be treated as "no error".
|
||||
eq(
|
||||
child.lua_get([[V.chain(
|
||||
function(v) return "" end,
|
||||
V.non_empty()
|
||||
)("")]]),
|
||||
"This field is required"
|
||||
)
|
||||
end
|
||||
|
||||
return T
|
||||
Reference in New Issue
Block a user