import { expect, test, vi } from "vitest";
import { useRef, useState } from "react";
import { ErrorsMap, Field, FieldArray, Form } from "houseform";
import {
cleanup,
render,
waitFor,
waitForElementToBeRemoved,
} from "@testing-library/react";
import { z } from "zod";
import { FormInstance } from "houseform";
import * as React from "react";
test("Form should render children", () => {
const { getByText } = render(
);
expect(getByText("Test")).toBeInTheDocument();
});
test("Form should submit with basic values in tact", async () => {
const SubmitValues = () => {
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByText, container } = render( );
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{"email":"test@example.com"}
`)
);
});
test("Form should submit with simple array values in tact", async () => {
const SubmitValues = () => {
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByText, container } = render( );
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{"email":["test@example.com"]}
`)
);
});
test("Form should not submit if there are errors with onChangeValidate", async () => {
const SubmitValues = () => {
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByText } = render( );
await user.click(getByText("Submit"));
expect(getByText("Submit")).toBeInTheDocument();
});
test("Form should not submit if there are errors with onSubmitValidate", async () => {
const SubmitValues = () => {
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByText } = render( );
await user.click(getByText("Submit"));
expect(getByText("Submit")).toBeInTheDocument();
});
test("Form should show isValid proper", async () => {
const { findByText, getByPlaceholderText } = render(
);
expect(await findByText("Is valid")).toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(await findByText("Is not valid")).toBeInTheDocument();
});
test("Form should show isSubmitted proper", async () => {
const { getByText, findByText } = render(
);
expect(getByText("Not submitted")).toBeInTheDocument();
await user.click(getByText("Submit"));
expect(await findByText("Submitted")).toBeInTheDocument();
});
test("Form should show isTouched proper", async () => {
const { getByText, findByText } = render(
);
expect(getByText("Form is not touched")).toBeInTheDocument();
await user.click(getByText("Touch a field"));
expect(await findByText("Form is touched")).toBeInTheDocument();
});
test("Form should handle setIsTouched helper", async () => {
const { getByText, findByText } = render(
);
expect(getByText("Is not touched")).toBeInTheDocument();
await user.click(getByText("Touch"));
expect(await findByText("Is touched")).toBeInTheDocument();
});
test("Form should handle setIsDirty helper", async () => {
const { getByText, findByText } = render(
);
expect(getByText("Is not dirty")).toBeInTheDocument();
await user.click(getByText("Dirty"));
expect(await findByText("Is dirty")).toBeInTheDocument();
});
test("Form should reset isTouched when all touched fields are not touched anymore", async () => {
const { getByText, findByText } = render(
);
await user.click(getByText("Touch a field"));
expect(await findByText("Form is touched")).toBeInTheDocument();
await user.click(getByText("Untouch a field"));
expect(getByText("Form is not touched")).toBeInTheDocument();
});
test("Form should show isDirty proper", async () => {
const { getByText, findByText } = render(
);
expect(getByText("Form is not dirty")).toBeInTheDocument();
await user.click(getByText("Dirty a field"));
expect(await findByText("Form is dirty")).toBeInTheDocument();
});
test("Form should reset isDirty when all touched fields are not touched anymore", async () => {
const { getByText, findByText } = render(
);
await user.click(getByText("Dirty a field"));
expect(await findByText("Form is dirty")).toBeInTheDocument();
await user.click(getByText("Undirty a field"));
expect(getByText("Form is not dirty")).toBeInTheDocument();
});
test("Form should have context passed to ref", async () => {
const Comp = () => {
const formRef = useRef(undefined!);
const [val, setVal] = useState("");
if (val) return {val}
;
return (
setVal(formRef.current.getFieldValue("test")?.value)}
>
Submit
);
};
const { getByText, queryByText, findByText } = render( );
expect(queryByText("Test")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(await findByText("Test")).toBeInTheDocument();
});
test("Form submit should return `true` if valid", async () => {
const Comp = () => {
const [val, setVal] = useState(null);
if (val !== null) return {val ? "True" : "False"}
;
return (
);
};
const { getByText, queryByText, findByText } = render( );
expect(queryByText("True")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(await findByText("True")).toBeInTheDocument();
});
test("Form submit should return `false` if not valid", async () => {
const Comp = () => {
const [val, setVal] = useState(null);
if (val !== null) return {val ? "True" : "False"}
;
return (
);
};
const { getByText, queryByText, findByText } = render( );
expect(queryByText("False")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(await findByText("False")).toBeInTheDocument();
});
test("Field with dot notation should submit with deep object value", async () => {
const SubmitValues = () => {
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByText, container } = render( );
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{"test":{"other":{"email":"test@example.com"}}}
`)
);
});
test("Field with bracket notation should submit with deep object value", async () => {
const SubmitValues = () => {
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByText, container } = render( );
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{"test":{"other":{"email":"test@example.com"}}}
`)
);
});
// should be gotten with `getFieldValue('test.other')`
test("Form's `getFieldValue` should show dot notation for incorrect syntax", async () => {
const Comp = () => {
const formRef = useRef(undefined!);
const [val, setVal] = useState("");
if (val) return {val}
;
return (
setVal(formRef.current.getFieldValue('test["other"]')?.value)
}
>
Submit
);
};
const { getByText, queryByText, findByText } = render( );
expect(queryByText("Test")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(await findByText("Test")).toBeInTheDocument();
});
test("Form should show all field errors if requested", async () => {
const SubmitValues = () => {
return (
);
};
const { findByText } = render( );
expect(await findByText("Should have a min length of 1")).toBeInTheDocument();
expect(await findByText("Should have a min length of 3")).toBeInTheDocument();
});
test("Assigning a form to a ref should not break the application", async () => {
const Comp = () => {
const [formRef, setFormRef] = React.useState();
const setFormRefCB = React.useCallback((r: any) => {
setFormRef(r);
}, []);
return (
);
};
const { getByText } = render( );
expect(getByText("Testing")).toBeInTheDocument();
});
test("Form should not submit when errors are present", async () => {
const Comp = () => {
const [isSubmitted, setIsSubmitted] = useState(false);
if (isSubmitted) return Submitted
;
return (
);
};
const { getByText, queryByText } = render( );
await waitFor(() =>
expect(getByText("There are errors")).toBeInTheDocument()
);
await user.click(getByText("Submit"));
expect(queryByText("Submitted")).not.toBeInTheDocument();
});
test("Form should submit when errors are present and submitWhenInvalid is true", async () => {
const Comp = () => {
const [isSubmitted, setIsSubmitted] = useState(false);
if (isSubmitted) return Submitted
;
return (
);
};
const { getByText } = render( );
await waitFor(() =>
expect(getByText("There are errors")).toBeInTheDocument()
);
await user.click(getByText("Submit"));
expect(getByText("Submitted")).toBeInTheDocument();
});
test("Form submission should receive initially empty errors array", async () => {
const Comp = () => {
const [formErrors, setFormErrors] = useState(null);
if (formErrors !== null) {
return Form errors: {JSON.stringify(formErrors)}
;
}
return (
);
};
const { getByText, container } = render( );
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form errors/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
`);
});
test("Form submission should receive correct errors array when errors are in use in form itself", async () => {
const Comp = () => {
const [formErrors, setFormErrors] = useState(null);
if (formErrors !== null) {
return Form errors: {JSON.stringify(formErrors)}
;
}
return (
);
};
const { getByText, container } = render( );
await waitFor(() =>
expect(getByText(/There are errors/)).toBeInTheDocument()
);
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form errors/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
Form errors:
["You must have 12 characters"]
`);
});
test("Form submission should receive correct errors array when errors are not in use in form itself", async () => {
const Comp = () => {
const [formErrors, setFormErrors] = useState(null);
if (formErrors !== null) {
return Form errors: {JSON.stringify(formErrors)}
;
}
return (
);
};
const { getByText, container } = render( );
await waitFor(() =>
expect(getByText(/There are errors/)).toBeInTheDocument()
);
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form errors/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
Form errors:
["You must have 12 characters"]
`);
});
test("Form submission should receive correct isValid", async () => {
const Comp = () => {
const [formIsValid, setFormIsValid] = useState(null);
if (formIsValid !== null) {
return Form is valid: {formIsValid.toString()}
;
}
return (
);
};
const { getByText, container } = render( );
await waitFor(() =>
expect(getByText(/There are errors/)).toBeInTheDocument()
);
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form is valid/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
`);
});
test("Form submission should receive correct isTouched", async () => {
const Comp = () => {
const [formIsTouched, setFormIsTouched] = useState(null);
if (formIsTouched !== null) {
return Form is touched: {formIsTouched.toString()}
;
}
return (
);
};
const { getByText, container } = render( );
user.click(getByText("Blur me"));
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form is touched/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
`);
});
test("Form submission should receive correct isDirty", async () => {
const Comp = () => {
const [formIsDirty, setFormIsDirty] = useState(null);
if (formIsDirty !== null) {
return Form is dirty: {formIsDirty.toString()}
;
}
return (
);
};
const { getByText, container } = render( );
user.click(getByText("Set value"));
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form is dirty/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
`);
});
test("Form's memoChild should prevent re-renders", async () => {
const formNonMemoHasRendered = vi.fn();
const NonMemoComp = () => {
const [counter, setCounter] = useState(0);
return (
setCounter((v) => v + 1)}>Add to counter
Counter: {counter}
);
};
const { getByText: getByTextForNonMemo } = render( );
expect(getByTextForNonMemo("Counter: 0")).toBeInTheDocument();
expect(formNonMemoHasRendered).toHaveBeenCalledTimes(1);
user.click(getByTextForNonMemo("Add to counter"));
await waitFor(() =>
expect(getByTextForNonMemo("Counter: 1")).toBeInTheDocument()
);
expect(formNonMemoHasRendered).toHaveBeenCalledTimes(2);
cleanup();
const formMemoHasRendered = vi.fn();
const MemoComp = () => {
const [counter, setCounter] = useState(0);
return (
setCounter((v) => v + 1)}>Add to counter
Counter: {counter}
);
};
const { getByText: getByTextForMemo } = render( );
expect(getByTextForMemo("Counter: 0")).toBeInTheDocument();
expect(formMemoHasRendered).toHaveBeenCalledTimes(1);
user.click(getByTextForMemo("Add to counter"));
await waitFor(() =>
expect(getByTextForMemo("Counter: 1")).toBeInTheDocument()
);
expect(formMemoHasRendered).toHaveBeenCalledTimes(1);
});
test("Form errorsMap should show specific field errors only.", async () => {
const SubmitValues = () => {
return (
);
};
const { findByText, queryByText } = render( );
expect(await findByText("Should have a min length of 1")).toBeInTheDocument();
expect(queryByText("Should have a min length of 3")).not.toBeInTheDocument();
});
test("Form submission should receive initially empty errorsMap object", async () => {
const Comp = () => {
const [formErrorsMap, setFormErrorsMap] = useState(null);
if (formErrorsMap !== null) {
return Form errorsMap: {JSON.stringify(formErrorsMap)}
;
}
return (
);
};
const { getByText, container } = render( );
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form errors/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
`);
});
test("Form should set isValidating proper", async () => {
const { getByText, queryByText } = render(
);
expect(queryByText("Validating")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(getByText("Validating")).toBeInTheDocument();
await waitForElementToBeRemoved(() => queryByText("Validating"));
});
test("Form should reset with no backup values correctly", async () => {
const ResetValues = () => {
return (
);
};
const { getByText, container, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Email"), "test");
await user.type(getByPlaceholderText("Password"), "test");
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
`)
);
});
test("Form should reset with initial values correctly", async () => {
const ResetValues = () => {
return (
);
};
const { getByText, container, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Email"), "test");
await user.type(getByPlaceholderText("Password"), "test");
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
`)
);
});
test("Form should reset with resetWithValues correctly", async () => {
const ResetValues = () => {
return (
);
};
const { getByText, container, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Email"), "test");
await user.type(getByPlaceholderText("Password"), "test");
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
`)
);
});
test("Form should reset with empty string resetWithValue correctly", async () => {
const ResetValues = () => {
return (
);
};
const { getByText, container, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Email"), "test");
await user.type(getByPlaceholderText("Password"), "test");
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
`)
);
});
test("Form should reset with resetWithValues and initial values correctly", async () => {
const ResetValues = () => {
return (
);
};
const { getByText, container, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Email"), "test");
await user.type(getByPlaceholderText("Password"), "test");
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
`)
);
});
test("Form submission should receive FormInstance value", async () => {
const Comp = () => {
const [formValue, setFormValue] = useState | null>(
null
);
if (formValue !== null) {
return Form values: {JSON.stringify(formValue)}
;
}
return (
);
};
const { getByText, container } = render( );
user.click(getByText("Submit"));
await waitFor(() => expect(getByText(/Form values/)).toBeInTheDocument());
expect(container).toMatchInlineSnapshot(`
Form values:
{"test":"hello-world"}
`);
});
test("Form should use value to conditionally hide field based on another's value", async () => {
const Comp = () => {
return (
);
};
const { getByText, queryByText } = render( );
await waitFor(() => expect(getByText("I am here")).toBeInTheDocument());
user.click(getByText("Set"));
await waitFor(() => expect(queryByText("I am here")).toBeInTheDocument());
});
test("Form `deleteField` should remove field", async () => {
const Comp = () => {
const [show, setShow] = useState(true);
return (
);
};
const { rerender, getByText, queryByText } = render( );
rerender( );
expect(getByText("emailHere")).toBeInTheDocument();
await user.click(getByText("Unmount"));
rerender( );
expect(getByText("emailHere")).toBeInTheDocument();
await user.click(getByText("Delete field"));
rerender( );
expect(queryByText("emailHere")).not.toBeInTheDocument();
});
test("Form submit should reset errors", async () => {
const submitMock = vi.fn();
const { getByText, queryByText, getByPlaceholderText } = render(
);
await user.click(getByText("Submit"));
expect(
getByText("String must contain at least 1 character(s)")
).toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "emailhere");
await user.click(getByText("Submit"));
expect(
queryByText("String must contain at least 1 character(s)")
).not.toBeInTheDocument();
expect(submitMock).toHaveBeenCalledTimes(1);
});
test("Form should not trigger validation when reset", async () => {
const Comp = () => {
return (
);
};
const { getByText, getByPlaceholderText, queryByText } = render( );
await user.type(getByPlaceholderText("Email"), "emailHere");
await user.click(getByText("Reset"));
expect(queryByText("email error")).not.toBeInTheDocument();
});
test("onSubmitTransform should work with transform function", async () => {
const submitMock = vi.fn();
const { getByText, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Price"), "69");
await user.click(getByText("Submit"));
expect(submitMock.mock.calls[0][0]).toEqual({ price: 69 });
});
test("onSubmitTransform should work with async transform function", async () => {
const submitMock = vi.fn();
const { getByText, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Price"), "69");
await user.click(getByText("Submit"));
await waitFor(() => expect(submitMock).toBeCalledTimes(1));
expect(submitMock.mock.calls[0][0]).toEqual({ price: 69 });
});
test("onSubmitTransform should work with zod transform", async () => {
const submitMock = vi.fn();
const { getByText, getByPlaceholderText } = render(
);
await user.type(getByPlaceholderText("Price"), "69");
await user.click(getByText("Submit"));
expect(submitMock.mock.calls[0][0]).toEqual({ price: 69 });
});
test("onSubmitTransform should work with onSubmitValidate", async () => {
const submitMock = vi.fn();
const { getByText, getByPlaceholderText, queryByText } = render(
);
expect(queryByText("Validating")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Price"), "69");
await user.click(getByText("Submit"));
expect(getByText("Validating")).toBeInTheDocument();
await waitForElementToBeRemoved(() => queryByText("Validating"));
await waitFor(() => expect(submitMock).toBeCalledTimes(1));
expect(submitMock.mock.calls[0][0]).toEqual({ price: 69 });
});
test("onSubmitTransform should set errors and ignore value if it throws", async () => {
const submitMock = vi.fn();
const { getByText, getByPlaceholderText, findByText, queryByText } = render(
);
await user.click(getByText("Submit"));
expect(await findByText("Not valid")).toBeInTheDocument();
await waitFor(() => expect(submitMock).toBeCalledTimes(1));
expect(submitMock.mock.calls[0][0]).toEqual({});
await user.type(getByPlaceholderText("Price"), "69");
await user.click(getByText("Submit"));
await waitFor(() => expect(submitMock).toBeCalledTimes(2));
expect(queryByText("Not valid")).not.toBeInTheDocument();
expect(submitMock.mock.calls[1][0]).toEqual({ price: 69 });
});