Skip to content

Commit 1147348

Browse files
committed
test(react): Add form tests
1 parent 22b4b75 commit 1147348

File tree

2 files changed

+236
-1
lines changed

2 files changed

+236
-1
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
2+
import { render, screen, cleanup, renderHook, act, waitFor } from "@testing-library/react";
3+
import { form } from "./form";
4+
import { ComponentProps } from "react";
5+
6+
vi.mock("~/components/button", () => {
7+
return {
8+
Button: (props: ComponentProps<"button">) => <button {...props} data-testid="submit-button" />,
9+
};
10+
});
11+
12+
describe("form export", () => {
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
});
16+
17+
afterEach(() => {
18+
cleanup();
19+
});
20+
21+
it("should allow rendering of all composed components", () => {
22+
const { result } = renderHook(() => {
23+
return form.useAppForm({
24+
defaultValues: { foo: "bar" },
25+
});
26+
});
27+
28+
const hook = result.current;
29+
30+
render(
31+
<hook.AppForm>
32+
<hook.AppField name="foo" children={(field) => <field.Input label="Foo" />} />
33+
<hook.ErrorMessage />
34+
<hook.SubmitButton>Submit</hook.SubmitButton>
35+
<hook.Action>Action</hook.Action>
36+
</hook.AppForm>
37+
);
38+
39+
expect(screen.getByRole("textbox", { name: "Foo" })).toBeInTheDocument();
40+
expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
41+
expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
42+
expect(screen.getByText("Submit")).toBeInTheDocument();
43+
expect(screen.getByText("Action")).toBeInTheDocument();
44+
});
45+
46+
describe("<Input />", () => {
47+
it("should render the Input component", () => {
48+
const { result } = renderHook(() => {
49+
return form.useAppForm({
50+
defaultValues: { foo: "bar" },
51+
});
52+
});
53+
54+
const hook = result.current;
55+
56+
const { container } = render(
57+
<hook.AppForm>
58+
<hook.AppField name="foo" children={(field) => <field.Input label="Foo" />} />
59+
</hook.AppForm>
60+
);
61+
62+
expect(container.querySelector('label[for="foo"]')).toBeInTheDocument();
63+
expect(container.querySelector('label[for="foo"]')).toHaveTextContent("Foo");
64+
expect(container.querySelector('input[name="foo"]')).toBeInTheDocument();
65+
expect(container.querySelector('input[name="foo"]')).toHaveValue("bar");
66+
expect(container.querySelector('input[name="foo"]')).toHaveAttribute("aria-invalid", "false");
67+
});
68+
69+
it("should render the Input children when provided", () => {
70+
const { result } = renderHook(() => {
71+
return form.useAppForm({
72+
defaultValues: { foo: "bar" },
73+
});
74+
});
75+
76+
const hook = result.current;
77+
78+
render(
79+
<hook.AppForm>
80+
<hook.AppField
81+
name="foo"
82+
children={(field) => (
83+
<field.Input label="Foo">
84+
<div data-testid="test-child">Test Child</div>
85+
</field.Input>
86+
)}
87+
/>
88+
</hook.AppForm>
89+
);
90+
91+
expect(screen.getByTestId("test-child")).toBeInTheDocument();
92+
});
93+
94+
it("should render the Input metadata when available", async () => {
95+
const { result } = renderHook(() => {
96+
return form.useAppForm({
97+
defaultValues: { foo: "" },
98+
});
99+
});
100+
101+
const hook = result.current;
102+
103+
render(
104+
<hook.AppForm>
105+
<hook.AppField
106+
validators={{
107+
onSubmit: () => {
108+
return "error!";
109+
},
110+
}}
111+
name="foo"
112+
children={(field) => <field.Input label="Foo" />}
113+
/>
114+
</hook.AppForm>
115+
);
116+
117+
await act(async () => {
118+
await hook.handleSubmit();
119+
});
120+
121+
const error = screen.getByRole("alert");
122+
expect(error).toBeInTheDocument();
123+
expect(error).toHaveClass("fui-form__error");
124+
});
125+
});
126+
127+
describe("<Action />", () => {
128+
it("should render the Action component", () => {
129+
const { result } = renderHook(() => {
130+
return form.useAppForm({});
131+
});
132+
133+
const hook = result.current;
134+
135+
render(
136+
<hook.AppForm>
137+
<hook.Action>Action</hook.Action>
138+
</hook.AppForm>
139+
);
140+
141+
expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
142+
expect(screen.getByRole("button", { name: "Action" })).toHaveClass("fui-form__action");
143+
expect(screen.getByRole("button", { name: "Action" })).toHaveTextContent("Action");
144+
expect(screen.getByRole("button", { name: "Action" })).toHaveAttribute("type", "button");
145+
});
146+
});
147+
148+
describe("<SubmitButton />", () => {
149+
it("should render the SubmitButton component", () => {
150+
const { result } = renderHook(() => {
151+
return form.useAppForm({});
152+
});
153+
154+
const hook = result.current;
155+
156+
render(
157+
<hook.AppForm>
158+
<hook.SubmitButton>Submit</hook.SubmitButton>
159+
</hook.AppForm>
160+
);
161+
162+
expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
163+
expect(screen.getByRole("button", { name: "Submit" })).toHaveTextContent("Submit");
164+
expect(screen.getByRole("button", { name: "Submit" })).toHaveAttribute("type", "submit");
165+
expect(screen.getByTestId("submit-button")).toBeInTheDocument();
166+
});
167+
168+
it("should subscribe to the isSubmitting state", async () => {
169+
const { result } = renderHook(() => {
170+
return form.useAppForm({
171+
validators: {
172+
onSubmitAsync: async () => {
173+
// Simulate a slow async operation
174+
await new Promise((resolve) => setTimeout(resolve, 100));
175+
return undefined;
176+
},
177+
},
178+
});
179+
});
180+
181+
const hook = result.current;
182+
183+
render(
184+
<hook.AppForm>
185+
<hook.SubmitButton>Submit</hook.SubmitButton>
186+
</hook.AppForm>
187+
);
188+
189+
const submitButton = screen.getByTestId("submit-button");
190+
191+
expect(submitButton).toBeInTheDocument();
192+
expect(submitButton).not.toHaveAttribute("disabled");
193+
194+
act(() => {
195+
hook.handleSubmit();
196+
});
197+
198+
await waitFor(() => {
199+
expect(submitButton).toHaveAttribute("disabled");
200+
});
201+
});
202+
});
203+
204+
describe("<ErrorMessage />", () => {
205+
it.only("should render the ErrorMessage if the onSubmit error is set", async () => {
206+
const { result } = renderHook(() => {
207+
return form.useAppForm({
208+
validators: {
209+
onSubmitAsync: async () => {
210+
return "error!";
211+
},
212+
},
213+
});
214+
});
215+
216+
const hook = result.current;
217+
218+
const { container } = render(
219+
<hook.AppForm>
220+
<hook.ErrorMessage />
221+
</hook.AppForm>
222+
);
223+
224+
act(async () => {
225+
await hook.handleSubmit();
226+
});
227+
228+
await waitFor(() => {
229+
const error = container.querySelector(".fui-form__error");
230+
expect(error).toBeInTheDocument();
231+
expect(error).toHaveTextContent("error!");
232+
});
233+
});
234+
});
235+
});

packages/react/src/components/form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function FieldMetadata({ className, ...props }: ComponentProps<"div"> & { field:
99
if (!props.field.state.meta.isTouched || !props.field.state.meta.errors.length) {
1010
return null;
1111
}
12-
12+
1313
return (
1414
<div>
1515
<div role="alert" aria-live="polite" className={cn("fui-form__error", className)} {...props}>

0 commit comments

Comments
 (0)