fix(web): валидация записей шаблона — пустые values не уходят в API, ошибки видимы

This commit is contained in:
2026-07-03 18:10:10 +07:00
parent 99e09d35fb
commit 388bf4aeb6
2 changed files with 62 additions and 5 deletions
+38
View File
@@ -121,6 +121,44 @@ test("ошибка создания шаблона отображается по
expect(await screen.findByRole("alert")).toHaveTextContent("Не удалось создать шаблон") expect(await screen.findByRole("alert")).toHaveTextContent("Не удалось создать шаблон")
}) })
test("запись с нетронутым (пустым) values не уходит в api.createTemplate, показывается ошибка", async () => {
const createSpy = vi.spyOn(api, "createTemplate").mockClear()
const user = userEvent.setup()
renderPage()
await screen.findByText("Standard")
await user.type(screen.getByLabelText(/имя шаблона/i), "New")
await user.click(screen.getByRole("button", { name: /добавить запись/i }))
fireEvent.change(screen.getByLabelText(/имя записи 1/i), { target: { value: "www" } })
// значения записи намеренно не заполняются — остаётся дефолтная пустая строка
await user.click(screen.getByRole("button", { name: /сохранить шаблон/i }))
expect(await screen.findByRole("alert")).toHaveTextContent(/заполните имя и значения/i)
expect(createSpy).not.toHaveBeenCalled()
})
test("сабмит с пустым именем записи показывает ошибку и не вызывает api.createTemplate", async () => {
const createSpy = vi.spyOn(api, "createTemplate").mockClear()
const user = userEvent.setup()
renderPage()
await screen.findByText("Standard")
await user.type(screen.getByLabelText(/имя шаблона/i), "New")
await user.click(screen.getByRole("button", { name: /добавить запись/i }))
fireEvent.change(screen.getByLabelText(/значения записи 1/i), { target: { value: "1.1.1.1" } })
// имя записи намеренно оставлено пустым
await user.click(screen.getByRole("button", { name: /сохранить шаблон/i }))
expect(await screen.findByRole("alert")).toHaveTextContent(/заполните имя и значения/i)
expect(createSpy).not.toHaveBeenCalled()
})
test("пустое состояние при отсутствии шаблонов", async () => { test("пустое состояние при отсутствии шаблонов", async () => {
vi.spyOn(api, "listTemplates").mockResolvedValue([]) vi.spyOn(api, "listTemplates").mockResolvedValue([])
renderPage() renderPage()
+24 -5
View File
@@ -29,7 +29,9 @@ const recordSchema = z.object({
type: z.string().min(1), type: z.string().min(1),
name: z.string().min(1, "Укажите имя записи"), name: z.string().min(1, "Укажите имя записи"),
ttl: z.number().int("TTL — целое число").nonnegative("TTL не может быть отрицательным"), ttl: z.number().int("TTL — целое число").nonnegative("TTL не может быть отрицательным"),
values: z.array(z.string()), values: z
.array(z.string().trim().min(1, "Значение не может быть пустым"))
.min(1, "Добавьте хотя бы одно значение"),
}) })
const templateFormSchema = z.object({ const templateFormSchema = z.object({
@@ -41,6 +43,15 @@ type TemplateForm = z.infer<typeof templateFormSchema>
const EMPTY_FORM: TemplateForm = { name: "", records: [] } const EMPTY_FORM: TemplateForm = { name: "", records: [] }
function sanitizeRecords(records: TemplateForm["records"]) {
return records
.map((record) => ({
...record,
values: record.values.map((v) => v.trim()).filter(Boolean),
}))
.filter((record) => record.values.length > 0)
}
export function TemplatesPage() { export function TemplatesPage() {
const templates = useTemplates() const templates = useTemplates()
const createTemplate = useCreateTemplate() const createTemplate = useCreateTemplate()
@@ -73,9 +84,10 @@ export function TemplatesPage() {
} }
function onSubmit(values: TemplateForm) { function onSubmit(values: TemplateForm) {
const input = { ...values, records: sanitizeRecords(values.records) }
if (editingId) { if (editingId) {
updateTemplate.mutate( updateTemplate.mutate(
{ id: editingId, input: values }, { id: editingId, input },
{ {
onSuccess: () => { onSuccess: () => {
setEditingId(null) setEditingId(null)
@@ -84,7 +96,7 @@ export function TemplatesPage() {
}, },
) )
} else { } else {
createTemplate.mutate(values, { onSuccess: () => reset(EMPTY_FORM) }) createTemplate.mutate(input, { onSuccess: () => reset(EMPTY_FORM) })
} }
} }
@@ -96,6 +108,7 @@ export function TemplatesPage() {
} }
const saveMutation = editingId ? updateTemplate : createTemplate const saveMutation = editingId ? updateTemplate : createTemplate
const hasFormErrors = !!errors.name || !!errors.records
return ( return (
<div className="mx-auto flex max-w-4xl flex-col gap-6 px-6 py-8"> <div className="mx-auto flex max-w-4xl flex-col gap-6 px-6 py-8">
@@ -147,10 +160,16 @@ export function TemplatesPage() {
</FieldSet> </FieldSet>
<div className="flex items-center justify-between gap-3 border-t border-border pt-3"> <div className="flex items-center justify-between gap-3 border-t border-border pt-3">
{saveMutation.isError && ( {hasFormErrors ? (
<span role="alert" className="font-dns text-xs text-destructive"> <span role="alert" className="font-dns text-xs text-destructive">
{saveMutation.error.message} Заполните имя и значения всех записей
</span> </span>
) : (
saveMutation.isError && (
<span role="alert" className="font-dns text-xs text-destructive">
{saveMutation.error.message}
</span>
)
)} )}
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
{editingId && ( {editingId && (