import { expect, test, vi } from "vitest";
import {
fireEvent,
render,
waitFor,
waitForElementToBeRemoved,
} from "@testing-library/react";
import { Field, FieldInstance, Form, FormInstance } from "houseform";
import { z } from "zod";
import React, { useEffect, useRef, useState } from "react";
test("Field should render children", () => {
const { getByText } = render(
);
expect(getByText("Test")).toBeInTheDocument();
});
test("Field should render with initial values", async () => {
const { getByText } = render(
);
expect(getByText("test@example.com")).toBeInTheDocument();
});
test("Field should allow changing value", async () => {
const { getByPlaceholderText } = render(
);
const emailInput = getByPlaceholderText("Email");
expect(emailInput).toHaveValue("");
await user.type(emailInput, "test@example.com");
expect(emailInput).toHaveValue("test@example.com");
});
test("Field should show errors with async onChange validator function", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("This should show up")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("This should show up")).toBeInTheDocument();
});
test("Field should not show errors with valid input on an async onChange validator function", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("This is an error")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(queryByText("This is an error")).not.toBeInTheDocument();
});
test("Field should show errors with async onChange validator zod usage", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("You must input a valid email")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("You must input a valid email")).toBeInTheDocument();
});
test("Field should show errors with async onBlur validator zod usage", async () => {
const { getByPlaceholderText, queryByText, findByText } = render(
);
expect(queryByText(/There was an error/)).not.toBeInTheDocument();
fireEvent.change(getByPlaceholderText("Email"), {
target: { value: "test" },
});
expect(queryByText(/There was an error/)).not.toBeInTheDocument();
fireEvent.blur(getByPlaceholderText("Email"));
expect(await findByText(/There was an error/)).toBeInTheDocument();
});
test("Field should not show errors with async onChange validator zod usage", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("You must input a valid email")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test@gmail.com");
expect(queryByText("You must input a valid email")).not.toBeInTheDocument();
});
test("onSubmitValidate should work", async () => {
const { getByText, getByPlaceholderText, queryByText } = render(
);
expect(queryByText("Not valid")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(queryByText("Not valid")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(getByText("Not valid")).toBeInTheDocument();
});
test("Field onChange can clear an error when resolved", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("This is an error")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("This is an error")).toBeInTheDocument();
await user.clear(getByPlaceholderText("Email"));
await user.type(getByPlaceholderText("Email"), "true");
expect(queryByText("This is an error")).not.toBeInTheDocument();
});
test("Field can receive data from other fields", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Password Confirmation"), "test");
expect(getByText("Passwords must match")).toBeInTheDocument();
await user.clear(getByPlaceholderText("Password Confirmation"));
await user.type(getByPlaceholderText("Password Confirmation"), "testing123");
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
});
test("Field can check for onChangeValidate errors on submit", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Password Confirmation"), "test");
expect(getByText("Passwords must match")).toBeInTheDocument();
await user.clear(getByPlaceholderText("Password Confirmation"));
await user.type(getByPlaceholderText("Password Confirmation"), "testing123");
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Password"), "another");
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(getByText("Passwords must match")).toBeInTheDocument();
});
test("Field can check for onBlurValidate errors on submit", async () => {
const { getByText, queryByText } = render(
);
expect(queryByText(/There was an error/)).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(getByText(/There was an error/)).toBeInTheDocument();
});
test("Is touched should be set", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Touched")).not.toBeInTheDocument();
getByPlaceholderText("Email").focus();
getByPlaceholderText("Email").blur();
await waitFor(() => expect(getByText("Touched")).toBeInTheDocument());
});
test("Is dirty should be set", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Dirty")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("Dirty")).toBeInTheDocument();
});
test("Is dirty should be false if value is the initially provided", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Dirty")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("Dirty")).toBeInTheDocument();
await user.clear(getByPlaceholderText("Email"));
expect(queryByText("Dirty")).not.toBeInTheDocument();
});
test("Field can listen for changes in other fields to validate on multiple field changes - onChange", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Password Confirmation"), "testing123");
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.clear(getByPlaceholderText("Password"));
await user.type(getByPlaceholderText("Password"), "other");
expect(getByText("Passwords must match")).toBeInTheDocument();
});
test("Field can listen for changes in other fields to validate on multiple field changes - onBlur", async () => {
const { getByPlaceholderText, queryByText, findByText } = render(
);
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Password Confirmation"), "testing123");
fireEvent.blur(getByPlaceholderText("Password Confirmation"));
expect(queryByText("Passwords must match")).not.toBeInTheDocument();
await user.clear(getByPlaceholderText("Password"));
await user.type(getByPlaceholderText("Password"), "other");
fireEvent.blur(getByPlaceholderText("Password"));
expect(await findByText("Passwords must match")).toBeInTheDocument();
});
test("Field should have render props passed to ref", async () => {
const Comp = () => {
const fieldRef = useRef(undefined!);
const [val, setVal] = useState("");
if (val) return {val}
;
return (
);
};
const { getByText, queryByText, findByText } = render();
expect(queryByText("Test")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(await findByText("Test")).toBeInTheDocument();
});
test("Field should show errors with invalid onMount validator zod usage", async () => {
const { getByText } = render(
);
await waitFor(() =>
expect(getByText("You must input a valid email")).toBeInTheDocument()
);
});
test("Field should show not errors with valid onMount validator zod usage", async () => {
const Comp = () => {
const [didRun, setDidRun] = useState(false);
useEffect(() => {
if (!didRun) {
setTimeout(() => {
setDidRun(true);
}, 0);
}
}, [didRun, setDidRun]);
return (
<>
{didRun && Did run useEffect
}
>
);
};
const { queryByText, getByText } = render();
await waitFor(() =>
expect(getByText("Did run useEffect")).toBeInTheDocument()
);
expect(queryByText("You must input a valid email")).not.toBeInTheDocument();
});
test("Field should only run listeners when value changes", async () => {
const arr = Array.from({ length: 5 }, (_, i) => i);
const { getByTestId, getAllByText, queryAllByText } = render(
);
if (queryAllByText("Must be at least three")?.length) {
throw "Should not be present yet";
}
fireEvent.change(getByTestId("value1"), { target: { value: 0 } });
await waitFor(() =>
expect(getAllByText("Must be at least three")).toHaveLength(2)
);
});
test("Field should manually validate", async () => {
const Comp = () => {
const formRef = useRef(null);
const runValidate = () => {
formRef.current?.getFieldValue("test")?.validate("onChangeValidate");
};
return (
);
};
const { getByText, queryByText, findByText } = render();
expect(getByText("Value: Te")).toBeInTheDocument();
expect(queryByText("Must be at least three")).not.toBeInTheDocument();
await user.click(getByText("Validate"));
expect(await findByText("Must be at least three")).toBeInTheDocument();
});
test("Field should not throw error if manually validate against non-used validation type", async () => {
const Comp = () => {
const formRef = useRef(null);
const runValidate = () => {
formRef.current?.getFieldValue("test")?.validate("onBlurValidate");
};
return (
);
};
const { getByText, queryByText, findByText } = render();
expect(getByText("Value: Te")).toBeInTheDocument();
expect(queryByText("Must be at least three")).not.toBeInTheDocument();
await user.click(getByText("Validate"));
expect(queryByText("Must be at least three")).not.toBeInTheDocument();
});
test("isTouched should only change onBlur", async () => {
let touchedValue;
const { getByPlaceholderText } = render(
);
const emailInput = getByPlaceholderText("Email");
// Initially, isTouched should be false
expect(touchedValue).toBeFalsy();
// After changing the input value, isTouched should still be false
await fireEvent.change(emailInput, { target: { value: "test@example.com" } });
expect(touchedValue).toBeFalsy();
// After triggering the onBlur event, isTouched should be true
await fireEvent.blur(emailInput);
expect(touchedValue).toBeTruthy();
});
test("Field should set isValidating with async onMount validator function", async () => {
function isEmailUnique() {
return new Promise((resolve) => {
setTimeout(() => resolve(true), 50);
});
}
const { queryByText, getByText } = render(
);
await waitFor(() => expect(getByText("Validating")).toBeInTheDocument());
await waitForElementToBeRemoved(() => queryByText("Validating"));
});
test("Field should set isValidating with async onChange validator function", async () => {
function isEmailUnique() {
return new Promise((resolve) => {
setTimeout(() => resolve(true), 50);
});
}
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("Validating")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("Validating")).toBeInTheDocument();
await waitForElementToBeRemoved(() => queryByText("Validating"));
});
test("Field should set isValidating with async onBlur validator function", async () => {
function isEmailUnique() {
return new Promise((resolve) => {
setTimeout(() => resolve(true), 50);
});
}
const { getByPlaceholderText, queryByText, findByText } = render(
);
expect(queryByText("Validating")).not.toBeInTheDocument();
fireEvent.change(getByPlaceholderText("Email"), {
target: { value: "test" },
});
expect(queryByText("Validating")).not.toBeInTheDocument();
fireEvent.blur(getByPlaceholderText("Email"));
expect(await findByText("Validating")).toBeInTheDocument();
await waitForElementToBeRemoved(() => queryByText("Validating"));
});
test("Field should set isValidating with async onSubmit validator function", async () => {
function isEmailUnique() {
return new Promise((resolve) => {
setTimeout(() => resolve(true), 50);
});
}
const { getByText, queryByText } = render(
);
expect(queryByText("Validating")).not.toBeInTheDocument();
await user.click(getByText("submit"));
expect(getByText("Validating")).toBeInTheDocument();
await waitForElementToBeRemoved(() => queryByText("Validating"));
});
test("Field should remove its value when unrendered", async () => {
const Comp = () => {
const [show, setShow] = useState(true);
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByPlaceholderText, getByText, container } = render();
await user.type(getByPlaceholderText("Email"), "emailHere");
await user.type(getByPlaceholderText("Password"), "passwordHere");
await user.click(getByText("Unmount"));
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{"password":"passwordHere"}
`)
);
});
test("Field should not remove its value if preserveValue", async () => {
const Comp = () => {
const [show, setShow] = useState(true);
const [values, setValues] = useState(null);
if (values) return {values}
;
return (
);
};
const { getByPlaceholderText, getByText, container } = render();
await user.type(getByPlaceholderText("Email"), "emailHere");
await user.type(getByPlaceholderText("Password"), "passwordHere");
await user.click(getByText("Unmount"));
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{"email":"emailHere","password":"passwordHere"}
`)
);
});
test("Field should persist values when remounts if preserveValue", async () => {
const Comp = () => {
const [show, setShow] = useState(true);
return (
);
};
const { getByPlaceholderText, getByText, queryByText } = render();
expect(queryByText("emailHere")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "emailHere");
await user.click(getByText("Toggle mount"));
await user.click(getByText("Toggle mount"));
expect(getByText("emailHere")).toBeInTheDocument();
});
test("Field should not have duplication when remounts if preserveValue", async () => {
const Comp = () => {
const [show, setShow] = useState(true);
return (
);
};
const { getByText, queryByText, rerender } = render();
expect(queryByText("emailHere")).not.toBeInTheDocument();
await user.click(getByText("Toggle mount"));
await user.click(getByText("Toggle mount"));
rerender();
expect(getByText("Total fields: 1")).toBeInTheDocument();
});
test("Field should not be dirty if form is reset", async () => {
const { getByPlaceholderText, queryByText, getByText, findByText } = render(
);
expect(queryByText("Dirty")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("Dirty")).toBeInTheDocument();
await user.click(getByText("Reset"));
expect(queryByText("Dirty")).not.toBeInTheDocument();
});
test("Field should have the `isSubmitted` value", async () => {
const submitMock = vi.fn();
const { getByPlaceholderText, queryByText, getByText } = render(
);
const email = getByPlaceholderText("Email");
expect(queryByText("isSubmitted")).not.toBeInTheDocument();
await user.click(getByText("Submit"));
expect(getByText("isSubmitted")).toBeInTheDocument();
expect(submitMock).not.toHaveBeenCalled();
await user.type(email, "test@example.com");
await user.click(getByText("Submit"));
expect(getByText("isSubmitted")).toBeInTheDocument();
expect(submitMock).toHaveBeenCalled();
});
test("Field should recompute form context when unmounted", async () => {
const Comp = () => {
const [formProps, setFormProps] = useState(null);
if (formProps) return {formProps}
;
return (
);
};
const { getByPlaceholderText, getByRole, getByText, container } = render(
);
await user.click(getByRole("checkbox"));
await user.type(getByPlaceholderText("Conditional"), "toofew");
await user.click(getByRole("checkbox"));
await user.click(getByText("Submit"));
await waitFor(() =>
expect(container).toMatchInlineSnapshot(`
{
"isDirty": true,
"isTouched": true,
"isValid": true,
"isValidating": false,
"value": {
"isVisible": false
}
}
`)
);
});
test("Field should show hints with async onChange hint function", async () => {
const { getByPlaceholderText, queryByText, getByText } = render(
);
expect(queryByText("This should show up")).not.toBeInTheDocument();
await user.type(getByPlaceholderText("Email"), "test");
expect(getByText("This should show up")).toBeInTheDocument();
});
test("Field hints should not prevent an error", async () => {
const fn = vi.fn();
const { findByText, getByText } = render(
);
expect(await findByText("This should show up")).toBeInTheDocument();
await user.click(getByText("Submit"));
await waitFor(() => expect(fn).toHaveBeenCalled());
});