1 changed files with 304 additions and 0 deletions
@ -0,0 +1,304 @@ |
|||||
|
import { describe, expect, it, vi } from "vitest"; |
||||
|
import { fireEvent, render, screen, waitFor } from "@core/utils/test"; |
||||
|
import { DynamicForm } from "./DynamicForm"; |
||||
|
import { z } from "zod/v4"; |
||||
|
import { useAppStore } from "@core/stores/appStore.ts"; |
||||
|
|
||||
|
vi.mock("react-i18next", () => ({ |
||||
|
useTranslation: () => ({ |
||||
|
t: (key: string | string[]) => (Array.isArray(key) ? key[0] : key), |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
const addErrorMock = vi.fn(); |
||||
|
const removeErrorMock = vi.fn(); |
||||
|
|
||||
|
vi.mock("@core/stores/appStore.ts", () => ({ |
||||
|
useAppStore: () => ({ |
||||
|
addError: addErrorMock, |
||||
|
removeError: removeErrorMock, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
describe("DynamicForm", () => { |
||||
|
const schema = z.object({ |
||||
|
name: z.string().min(3, { message: "Too short" }), |
||||
|
}); |
||||
|
|
||||
|
const fieldGroups = [ |
||||
|
{ |
||||
|
label: "Test Group", |
||||
|
description: "Testing validation", |
||||
|
fields: [ |
||||
|
{ |
||||
|
type: "text", |
||||
|
id: "name", |
||||
|
name: "name", |
||||
|
label: "Name", |
||||
|
description: "Enter your name", |
||||
|
properties: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
it("shows validation error when input is too short", async () => { |
||||
|
render( |
||||
|
<DynamicForm<z.infer<typeof schema>> |
||||
|
onSubmit={vi.fn()} |
||||
|
validationSchema={schema} |
||||
|
defaultValues={{ name: "" }} |
||||
|
fieldGroups={fieldGroups} |
||||
|
/>, |
||||
|
); |
||||
|
const input = screen.getByLabelText("Name") as HTMLInputElement; |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "ab" } }); |
||||
|
|
||||
|
const error = await screen.findByText( |
||||
|
"formValidation.tooSmall.string", |
||||
|
); |
||||
|
expect(error).toBeVisible(); |
||||
|
}); |
||||
|
|
||||
|
it("clears validation error when input becomes valid", async () => { |
||||
|
render( |
||||
|
<DynamicForm<z.infer<typeof schema>> |
||||
|
onSubmit={vi.fn()} |
||||
|
validationSchema={schema} |
||||
|
defaultValues={{ name: "" }} |
||||
|
fieldGroups={fieldGroups} |
||||
|
/>, |
||||
|
); |
||||
|
const input = screen.getByLabelText("Name") as HTMLInputElement; |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "ab" } }); |
||||
|
expect( |
||||
|
await screen.findByText("formValidation.tooSmall.string"), |
||||
|
).toBeVisible(); |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "abcd" } }); |
||||
|
await waitFor(() => |
||||
|
expect( |
||||
|
screen.queryByText("formValidation.tooSmall.string"), |
||||
|
).toBeNull() |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it("calls onSubmit when form is valid onChange", async () => { |
||||
|
const onSubmit = vi.fn(); |
||||
|
render( |
||||
|
<DynamicForm<z.infer<typeof schema>> |
||||
|
onSubmit={onSubmit} |
||||
|
validationSchema={schema} |
||||
|
defaultValues={{ name: "" }} |
||||
|
fieldGroups={fieldGroups} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const input = screen.getByLabelText("Name") as HTMLInputElement; |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "ab" } }); |
||||
|
expect( |
||||
|
await screen.findByText("formValidation.tooSmall.string"), |
||||
|
).toBeVisible(); |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "abcd" } }); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(onSubmit).toHaveBeenCalledTimes(1); |
||||
|
}); |
||||
|
|
||||
|
expect(onSubmit).toHaveBeenCalledWith( |
||||
|
{ name: "abcd" }, |
||||
|
expect.any(Object), |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it("renders a button and only calls onSubmit on click with submitType='onSubmit'", async () => { |
||||
|
const onSubmit = vi.fn(); |
||||
|
render( |
||||
|
<DynamicForm<z.infer<typeof schema>> |
||||
|
onSubmit={onSubmit} |
||||
|
submitType="onSubmit" |
||||
|
hasSubmitButton={true} |
||||
|
validationSchema={schema} |
||||
|
defaultValues={{ name: "" }} |
||||
|
fieldGroups={fieldGroups} |
||||
|
/>, |
||||
|
); |
||||
|
|
||||
|
const btn = screen.getByRole("button", { name: /submit/i }); |
||||
|
expect(btn).toBeInTheDocument(); |
||||
|
|
||||
|
fireEvent.input(screen.getByLabelText("Name"), { target: { value: "ab" } }); |
||||
|
await screen.findByText("formValidation.tooSmall.string"); |
||||
|
fireEvent.click(btn); |
||||
|
expect(onSubmit).not.toHaveBeenCalled(); |
||||
|
|
||||
|
fireEvent.input(screen.getByLabelText("Name"), { |
||||
|
target: { value: "abcd" }, |
||||
|
}); |
||||
|
await waitFor(() => |
||||
|
expect(screen.queryByText("formValidation.tooSmall.string")).toBeNull() |
||||
|
); |
||||
|
fireEvent.click(btn); |
||||
|
|
||||
|
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); |
||||
|
expect(onSubmit).toHaveBeenCalledWith({ name: "abcd" }, expect.any(Object)); |
||||
|
}); |
||||
|
|
||||
|
it("renders defaultValues correctly", () => { |
||||
|
render( |
||||
|
<DynamicForm<{ name: string }> |
||||
|
onSubmit={vi.fn()} |
||||
|
// no validationSchema
|
||||
|
defaultValues={{ name: "Alice" }} |
||||
|
fieldGroups={[ |
||||
|
{ |
||||
|
label: "Group", |
||||
|
description: "", |
||||
|
fields: [ |
||||
|
{ |
||||
|
type: "text", |
||||
|
name: "name", |
||||
|
label: "Name", |
||||
|
description: "", |
||||
|
properties: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]} |
||||
|
/>, |
||||
|
); |
||||
|
const input = screen.getByLabelText("Name") as HTMLInputElement; |
||||
|
expect(input.value).toBe("Alice"); |
||||
|
}); |
||||
|
|
||||
|
it("toggles disabled state based on disabledBy rules", async () => { |
||||
|
const schema = z.object({ |
||||
|
enable: z.boolean(), |
||||
|
follow: z.string(), |
||||
|
}); |
||||
|
render( |
||||
|
<DynamicForm<z.infer<typeof schema>> |
||||
|
onSubmit={vi.fn()} |
||||
|
validationSchema={schema} |
||||
|
defaultValues={{ enable: false, follow: "" }} |
||||
|
fieldGroups={[ |
||||
|
{ |
||||
|
label: "Group", |
||||
|
description: "", |
||||
|
fields: [ |
||||
|
{ |
||||
|
type: "toggle", |
||||
|
name: "enable", |
||||
|
label: "enable", |
||||
|
description: "", |
||||
|
}, |
||||
|
{ |
||||
|
type: "text", |
||||
|
name: "follow", |
||||
|
label: "follow", |
||||
|
description: "", |
||||
|
disabledBy: [{ fieldName: "enable" }], |
||||
|
properties: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]} |
||||
|
/>, |
||||
|
); |
||||
|
const enable = screen.getByRole("switch", { |
||||
|
name: "enable", |
||||
|
}) as HTMLInputElement; |
||||
|
|
||||
|
const follow = screen.getByLabelText("follow") as HTMLInputElement; |
||||
|
await waitFor(() => { |
||||
|
expect(enable.getAttribute("aria-checked")).toBe("false"); |
||||
|
expect(follow).toBeDisabled(); |
||||
|
}); |
||||
|
|
||||
|
fireEvent.click(enable); |
||||
|
await waitFor(() => { |
||||
|
expect(enable.getAttribute("aria-checked")).toBe("true"); |
||||
|
expect(follow).not.toBeDisabled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("always calls onSubmit onChange when no validationSchema is provided", async () => { |
||||
|
const onSubmit = vi.fn(); |
||||
|
render( |
||||
|
<DynamicForm<{ foo: string }> |
||||
|
onSubmit={onSubmit} |
||||
|
// no validationSchema
|
||||
|
defaultValues={{ foo: "" }} |
||||
|
fieldGroups={[ |
||||
|
{ |
||||
|
label: "G", |
||||
|
description: "", |
||||
|
fields: [ |
||||
|
{ |
||||
|
type: "text", |
||||
|
name: "foo", |
||||
|
label: "Foo", |
||||
|
description: "", |
||||
|
properties: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]} |
||||
|
/>, |
||||
|
); |
||||
|
const input = screen.getByLabelText("Foo") as HTMLInputElement; |
||||
|
fireEvent.input(input, { target: { value: "bar" } }); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(onSubmit).toHaveBeenCalledTimes(1); |
||||
|
expect(onSubmit).toHaveBeenCalledWith({ foo: "bar" }, expect.any(Object)); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("syncs errors to appStore when formId is set", async () => { |
||||
|
const { addError, removeError } = useAppStore(); |
||||
|
const schema = z.object({ foo: z.string().min(2) }); |
||||
|
const groups = [ |
||||
|
{ |
||||
|
label: "G", |
||||
|
description: "", |
||||
|
fields: [ |
||||
|
{ |
||||
|
type: "text", |
||||
|
name: "foo", |
||||
|
label: "Foo", |
||||
|
description: "", |
||||
|
properties: {}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
render( |
||||
|
<DynamicForm<z.infer<typeof schema>> |
||||
|
onSubmit={vi.fn()} |
||||
|
formId="myForm" |
||||
|
validationSchema={schema} |
||||
|
defaultValues={{ foo: "" }} |
||||
|
fieldGroups={groups} |
||||
|
/>, |
||||
|
); |
||||
|
const input = screen.getByLabelText("Foo") as HTMLInputElement; |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "a" } }); |
||||
|
await screen.findByText(/tooSmall/i); |
||||
|
|
||||
|
expect(addError).toHaveBeenCalledWith("foo", ""); |
||||
|
expect(addError).toHaveBeenCalledWith("myForm", ""); |
||||
|
|
||||
|
fireEvent.input(input, { target: { value: "abc" } }); |
||||
|
await waitFor(() => { |
||||
|
expect(removeError).toHaveBeenCalledWith("foo"); |
||||
|
expect(removeError).toHaveBeenCalledWith("myForm"); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue