fix(web): валидация записей шаблона — пустые values не уходят в API, ошибки видимы
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user