@zeno-lib/forms 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-zeno-form.d.mts +118 -0
- package/dist/create-zeno-form.mjs +121 -0
- package/dist/form-element.d.mts +13 -0
- package/dist/form-element.mjs +31 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +10 -0
- package/dist/lib/action-result.d.mts +22 -0
- package/dist/lib/action-result.mjs +32 -0
- package/dist/lib/apply-validation-error.d.mts +6 -0
- package/dist/lib/apply-validation-error.mjs +41 -0
- package/dist/lib/aria.d.mts +4 -0
- package/dist/lib/aria.mjs +7 -0
- package/dist/lib/contexts.d.mts +5 -0
- package/dist/lib/contexts.mjs +7 -0
- package/dist/lib/formatted-number.d.mts +56 -0
- package/dist/lib/formatted-number.mjs +138 -0
- package/dist/lib/schema-defaults.d.mts +17 -0
- package/dist/lib/schema-defaults.mjs +75 -0
- package/dist/lib/schema-required.d.mts +25 -0
- package/dist/lib/schema-required.mjs +72 -0
- package/dist/lib/submit-action.d.mts +64 -0
- package/dist/lib/submit-action.mjs +43 -0
- package/dist/lib/use-form-dialog.d.mts +73 -0
- package/dist/lib/use-form-dialog.mjs +90 -0
- package/dist/lib/use-formatted-number.d.mts +61 -0
- package/dist/lib/use-formatted-number.mjs +104 -0
- package/dist/lib/use-is-invalid.d.mts +7 -0
- package/dist/lib/use-is-invalid.mjs +31 -0
- package/dist/lib/use-rebased-default-values.d.mts +10 -0
- package/dist/lib/use-rebased-default-values.mjs +21 -0
- package/dist/lib/use-unsaved-changes-warning.d.mts +6 -0
- package/dist/lib/use-unsaved-changes-warning.mjs +24 -0
- package/dist/lib/validation-error.d.mts +12 -0
- package/dist/lib/validation-error.mjs +11 -0
- package/dist/lib/validation-logic.d.mts +5 -0
- package/dist/lib/validation-logic.mjs +114 -0
- package/dist/lib/validation-modes.d.mts +16 -0
- package/dist/lib/validation-modes.mjs +38 -0
- package/dist/tanstack.d.mts +1 -0
- package/dist/tanstack.mjs +2 -0
- package/package.json +25 -7
- package/src/create-form.tsx +10 -0
- package/src/fields/checkbox-group-field.tsx +160 -0
- package/src/fields/index.ts +5 -0
- package/src/fields/money-field.tsx +123 -0
- package/src/fields/multi-select-field.tsx +193 -0
- package/src/fields/percentage-field.tsx +73 -0
- package/src/fields/year-field.tsx +56 -0
- package/src/form-dialog.test.tsx +161 -0
- package/src/form-dialog.tsx +253 -0
- package/src/formatted-number-fields.test.tsx +236 -0
- package/src/index.ts +13 -0
- package/src/lib/action-result.ts +61 -0
- package/src/lib/apply-validation-error.test.ts +15 -4
- package/src/lib/apply-validation-error.ts +38 -6
- package/src/lib/formatted-number.test.ts +157 -0
- package/src/lib/formatted-number.ts +307 -0
- package/src/lib/submit-action.test-d.ts +83 -0
- package/src/lib/submit-action.ts +178 -0
- package/src/lib/use-form-dialog.ts +184 -0
- package/src/lib/use-formatted-number.ts +235 -0
- package/src/multi-value-fields.test.tsx +116 -0
- package/src/submit-action.test.tsx +421 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
|
|
2
|
+
import userEvent from "@zeno-lib/test/user-event"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { afterEach, describe, expect, test, vi } from "vitest"
|
|
5
|
+
import { z } from "zod"
|
|
6
|
+
import { Form, FormProvider, useForm } from "./create-form"
|
|
7
|
+
import type { ActionResult } from "./lib/action-result"
|
|
8
|
+
import { submitAction } from "./lib/submit-action"
|
|
9
|
+
import { ValidationError } from "./lib/validation-error"
|
|
10
|
+
import type { ValidationMode } from "./lib/validation-modes"
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const SUBMIT = { name: "Submit" }
|
|
17
|
+
const EMAIL_LABEL = /Email/
|
|
18
|
+
const NAME_LABEL = /Name/
|
|
19
|
+
const OWNER_1_SHARE_LABEL = /^Owner 1 share/
|
|
20
|
+
const OWNER_2_SHARE_LABEL = /^Owner 2 share/
|
|
21
|
+
|
|
22
|
+
function failure(
|
|
23
|
+
fieldErrors: Record<string, string[]>,
|
|
24
|
+
formErrors: string[] = []
|
|
25
|
+
): ActionResult<never> {
|
|
26
|
+
return { error: { fieldErrors, formErrors }, ok: false }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const profileSchema = z.object({
|
|
30
|
+
email: z.email("Enter a valid email"),
|
|
31
|
+
name: z.string().min(1, "Required"),
|
|
32
|
+
})
|
|
33
|
+
type Profile = z.infer<typeof profileSchema>
|
|
34
|
+
|
|
35
|
+
function ProfileForm({
|
|
36
|
+
action,
|
|
37
|
+
formRef,
|
|
38
|
+
validators,
|
|
39
|
+
}: {
|
|
40
|
+
action: (input: Profile) => Promise<ActionResult<unknown>>
|
|
41
|
+
formRef?: { current: unknown }
|
|
42
|
+
validators?: ValidationMode
|
|
43
|
+
}) {
|
|
44
|
+
const form = useForm({
|
|
45
|
+
defaultValues: { email: "ada@example.com", name: "Ada" },
|
|
46
|
+
onSubmit: (submit) => submitAction(submit, action),
|
|
47
|
+
schema: profileSchema,
|
|
48
|
+
...(validators ? { validators } : {}),
|
|
49
|
+
})
|
|
50
|
+
if (formRef) {
|
|
51
|
+
formRef.current = form
|
|
52
|
+
}
|
|
53
|
+
const { EmailField, InputField, SubmitButton } = form
|
|
54
|
+
return (
|
|
55
|
+
<FormProvider form={form}>
|
|
56
|
+
<Form>
|
|
57
|
+
<InputField label="Name" name="name" />
|
|
58
|
+
<EmailField label="Email" name="email" />
|
|
59
|
+
<SubmitButton>Submit</SubmitButton>
|
|
60
|
+
</Form>
|
|
61
|
+
</FormProvider>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("submitAction — field errors from the action", () => {
|
|
66
|
+
const modes: ValidationMode[] = [
|
|
67
|
+
"blur-then-change",
|
|
68
|
+
"change",
|
|
69
|
+
"blur",
|
|
70
|
+
"submit",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for (const mode of modes) {
|
|
74
|
+
test(`${mode}: an error lands on its field and clears when that field is edited, not another`, async () => {
|
|
75
|
+
const user = userEvent.setup()
|
|
76
|
+
const action = vi.fn(() =>
|
|
77
|
+
Promise.resolve(
|
|
78
|
+
failure({ email: ["Already taken"], name: ["Name is reserved"] })
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
render(<ProfileForm action={action} validators={mode} />)
|
|
82
|
+
|
|
83
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
84
|
+
|
|
85
|
+
expect(await screen.findByText("Already taken")).not.toBeNull()
|
|
86
|
+
expect(screen.getByText("Name is reserved")).not.toBeNull()
|
|
87
|
+
|
|
88
|
+
// Editing the email clears only the email's server message.
|
|
89
|
+
await user.type(screen.getByLabelText(EMAIL_LABEL), "m")
|
|
90
|
+
expect(screen.queryByText("Already taken")).toBeNull()
|
|
91
|
+
expect(screen.getByText("Name is reserved")).not.toBeNull()
|
|
92
|
+
|
|
93
|
+
await user.type(screen.getByLabelText(NAME_LABEL), "x")
|
|
94
|
+
expect(screen.queryByText("Name is reserved")).toBeNull()
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("a field with a server error blocks resubmission until it is edited", async () => {
|
|
99
|
+
const user = userEvent.setup()
|
|
100
|
+
const action = vi
|
|
101
|
+
.fn<(input: Profile) => Promise<ActionResult<unknown>>>()
|
|
102
|
+
.mockResolvedValueOnce(failure({ email: ["Already taken"] }))
|
|
103
|
+
.mockResolvedValue({ data: null, ok: true })
|
|
104
|
+
render(<ProfileForm action={action} />)
|
|
105
|
+
|
|
106
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
107
|
+
await screen.findByText("Already taken")
|
|
108
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
109
|
+
expect(action).toHaveBeenCalledTimes(1)
|
|
110
|
+
|
|
111
|
+
await user.type(screen.getByLabelText(EMAIL_LABEL), "m")
|
|
112
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
113
|
+
expect(action).toHaveBeenCalledTimes(2)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("array-index paths reach the field registered under that name", async () => {
|
|
117
|
+
const user = userEvent.setup()
|
|
118
|
+
const schema = z.object({
|
|
119
|
+
owners: z.array(z.object({ name: z.string(), percentage: z.string() })),
|
|
120
|
+
})
|
|
121
|
+
function Harness() {
|
|
122
|
+
const form = useForm({
|
|
123
|
+
defaultValues: {
|
|
124
|
+
owners: [
|
|
125
|
+
{ name: "Ada", percentage: "60" },
|
|
126
|
+
{ name: "Grace", percentage: "40" },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
onSubmit: (submit) =>
|
|
130
|
+
submitAction(submit, () =>
|
|
131
|
+
Promise.resolve(
|
|
132
|
+
failure({
|
|
133
|
+
"owners[1].percentage": ["Exceeds the remaining share"],
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
),
|
|
137
|
+
schema,
|
|
138
|
+
})
|
|
139
|
+
const { InputField, SubmitButton } = form
|
|
140
|
+
return (
|
|
141
|
+
<FormProvider form={form}>
|
|
142
|
+
<Form>
|
|
143
|
+
<InputField label="Owner 1 share" name="owners[0].percentage" />
|
|
144
|
+
<InputField label="Owner 2 share" name="owners[1].percentage" />
|
|
145
|
+
<SubmitButton>Submit</SubmitButton>
|
|
146
|
+
</Form>
|
|
147
|
+
</FormProvider>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
render(<Harness />)
|
|
151
|
+
|
|
152
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
153
|
+
|
|
154
|
+
const message = await screen.findByText("Exceeds the remaining share")
|
|
155
|
+
const second = screen.getByLabelText(OWNER_2_SHARE_LABEL)
|
|
156
|
+
expect(second.getAttribute("aria-invalid")).toBe("true")
|
|
157
|
+
expect(
|
|
158
|
+
screen.getByLabelText(OWNER_1_SHARE_LABEL).getAttribute("aria-invalid")
|
|
159
|
+
).not.toBe("true")
|
|
160
|
+
expect(message).not.toBeNull()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test("form errors and messages for unmounted names become the form-level error", async () => {
|
|
164
|
+
const user = userEvent.setup()
|
|
165
|
+
const formRef: { current: unknown } = { current: undefined }
|
|
166
|
+
const action = () =>
|
|
167
|
+
Promise.resolve(
|
|
168
|
+
failure({ "owners[3].name": ["Unknown owner"] }, ["Record is locked"])
|
|
169
|
+
)
|
|
170
|
+
render(<ProfileForm action={action} formRef={formRef} />)
|
|
171
|
+
|
|
172
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
173
|
+
|
|
174
|
+
await vi.waitFor(() => {
|
|
175
|
+
const form = formRef.current as { state: { errorMap: unknown } }
|
|
176
|
+
expect(form.state.errorMap).toMatchObject({
|
|
177
|
+
onSubmit: "Record is locked\nUnknown owner",
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
const form = formRef.current as {
|
|
181
|
+
state: { fieldMeta: Record<string, unknown> }
|
|
182
|
+
}
|
|
183
|
+
expect(form.state.fieldMeta["owners[3].name"]).toBeUndefined()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test("a ValidationError thrown from onSubmit also clears when its field is edited", async () => {
|
|
187
|
+
const user = userEvent.setup()
|
|
188
|
+
function Harness() {
|
|
189
|
+
const form = useForm({
|
|
190
|
+
defaultValues: { email: "ada@example.com", name: "Ada" },
|
|
191
|
+
onSubmit: () => {
|
|
192
|
+
throw new ValidationError({ email: "Taken on the server" })
|
|
193
|
+
},
|
|
194
|
+
schema: profileSchema,
|
|
195
|
+
})
|
|
196
|
+
const { EmailField, InputField, SubmitButton } = form
|
|
197
|
+
return (
|
|
198
|
+
<FormProvider form={form}>
|
|
199
|
+
<Form>
|
|
200
|
+
<InputField label="Name" name="name" />
|
|
201
|
+
<EmailField label="Email" name="email" />
|
|
202
|
+
<SubmitButton>Submit</SubmitButton>
|
|
203
|
+
</Form>
|
|
204
|
+
</FormProvider>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
render(<Harness />)
|
|
208
|
+
|
|
209
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
210
|
+
expect(await screen.findByText("Taken on the server")).not.toBeNull()
|
|
211
|
+
|
|
212
|
+
await user.type(screen.getByLabelText(NAME_LABEL), "x")
|
|
213
|
+
expect(screen.getByText("Taken on the server")).not.toBeNull()
|
|
214
|
+
|
|
215
|
+
await user.type(screen.getByLabelText(EMAIL_LABEL), "m")
|
|
216
|
+
expect(screen.queryByText("Taken on the server")).toBeNull()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test("several messages for one field all render", async () => {
|
|
220
|
+
const user = userEvent.setup()
|
|
221
|
+
const action = () =>
|
|
222
|
+
Promise.resolve(
|
|
223
|
+
failure({ email: ["Already taken", "Domain is blocked"] })
|
|
224
|
+
)
|
|
225
|
+
render(<ProfileForm action={action} />)
|
|
226
|
+
|
|
227
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
228
|
+
|
|
229
|
+
expect(await screen.findByText("Already taken")).not.toBeNull()
|
|
230
|
+
expect(screen.getByText("Domain is blocked")).not.toBeNull()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test("a thrown action error propagates and applies nothing", async () => {
|
|
234
|
+
const boom = new Error("network down")
|
|
235
|
+
const run = submitAction(
|
|
236
|
+
{
|
|
237
|
+
formApi: {} as never,
|
|
238
|
+
value: { email: "a@b.co", name: "A" },
|
|
239
|
+
},
|
|
240
|
+
() => Promise.reject(boom)
|
|
241
|
+
)
|
|
242
|
+
await expect(run).rejects.toBe(boom)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe("submitAction — schema", () => {
|
|
247
|
+
test("passes the schema output to the action, not the input values", async () => {
|
|
248
|
+
const user = userEvent.setup()
|
|
249
|
+
const schema = z.object({
|
|
250
|
+
percentage: z.string().transform((value) => Number(value)),
|
|
251
|
+
})
|
|
252
|
+
const action = vi.fn((input: { percentage: number }) =>
|
|
253
|
+
Promise.resolve<ActionResult<number>>({
|
|
254
|
+
data: input.percentage,
|
|
255
|
+
ok: true,
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
function Harness() {
|
|
259
|
+
const form = useForm({
|
|
260
|
+
defaultValues: { percentage: "42" },
|
|
261
|
+
onSubmit: (submit) => submitAction(submit, action, { schema }),
|
|
262
|
+
})
|
|
263
|
+
const { InputField, SubmitButton } = form
|
|
264
|
+
return (
|
|
265
|
+
<FormProvider form={form}>
|
|
266
|
+
<Form>
|
|
267
|
+
<InputField label="Share" name="percentage" />
|
|
268
|
+
<SubmitButton>Submit</SubmitButton>
|
|
269
|
+
</Form>
|
|
270
|
+
</FormProvider>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
render(<Harness />)
|
|
274
|
+
|
|
275
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
276
|
+
|
|
277
|
+
await vi.waitFor(() =>
|
|
278
|
+
expect(action).toHaveBeenCalledWith({ percentage: 42 })
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test("a client-side schema failure is applied and the action is not called", async () => {
|
|
283
|
+
const user = userEvent.setup()
|
|
284
|
+
const schema = z.object({ name: z.string().min(3, "At least 3") })
|
|
285
|
+
const action = vi.fn(() =>
|
|
286
|
+
Promise.resolve<ActionResult<null>>({ data: null, ok: true })
|
|
287
|
+
)
|
|
288
|
+
let result: ActionResult<null> | undefined
|
|
289
|
+
function Harness() {
|
|
290
|
+
const form = useForm({
|
|
291
|
+
defaultValues: { name: "Al" },
|
|
292
|
+
onSubmit: async (submit) => {
|
|
293
|
+
result = await submitAction(submit, action, { schema })
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
const { InputField, SubmitButton } = form
|
|
297
|
+
return (
|
|
298
|
+
<FormProvider form={form}>
|
|
299
|
+
<Form>
|
|
300
|
+
<InputField label="Name" name="name" />
|
|
301
|
+
<SubmitButton>Submit</SubmitButton>
|
|
302
|
+
</Form>
|
|
303
|
+
</FormProvider>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
render(<Harness />)
|
|
307
|
+
|
|
308
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
309
|
+
|
|
310
|
+
expect(await screen.findByText("At least 3")).not.toBeNull()
|
|
311
|
+
expect(action).not.toHaveBeenCalled()
|
|
312
|
+
expect(result).toEqual({
|
|
313
|
+
error: { fieldErrors: { name: ["At least 3"] }, formErrors: [] },
|
|
314
|
+
ok: false,
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe("submitAction — success", () => {
|
|
320
|
+
type FormState = {
|
|
321
|
+
state: {
|
|
322
|
+
isDefaultValue: boolean
|
|
323
|
+
isDirty: boolean
|
|
324
|
+
values: Profile
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// The page re-reading the saved record, as `revalidatePath` does for a
|
|
329
|
+
// server component: `defaultValues` follows what the action saved.
|
|
330
|
+
function SavedForm({
|
|
331
|
+
formRef,
|
|
332
|
+
reset,
|
|
333
|
+
}: {
|
|
334
|
+
formRef: { current: unknown }
|
|
335
|
+
reset?: "values" | true
|
|
336
|
+
}) {
|
|
337
|
+
const [saved, setSaved] = useState<Profile>({
|
|
338
|
+
email: "ada@example.com",
|
|
339
|
+
name: "Ada",
|
|
340
|
+
})
|
|
341
|
+
const save = (input: Profile) => {
|
|
342
|
+
const record = { ...input, name: input.name.trim() }
|
|
343
|
+
setSaved(record)
|
|
344
|
+
return Promise.resolve<ActionResult<Profile>>({ data: record, ok: true })
|
|
345
|
+
}
|
|
346
|
+
const form = useForm({
|
|
347
|
+
defaultValues: saved,
|
|
348
|
+
onSubmit: (submit) => submitAction(submit, save, reset ? { reset } : {}),
|
|
349
|
+
schema: profileSchema,
|
|
350
|
+
})
|
|
351
|
+
formRef.current = form
|
|
352
|
+
const { EmailField, InputField, SubmitButton } = form
|
|
353
|
+
return (
|
|
354
|
+
<FormProvider form={form}>
|
|
355
|
+
<Form>
|
|
356
|
+
<InputField label="Name" name="name" />
|
|
357
|
+
<EmailField label="Email" name="email" />
|
|
358
|
+
<SubmitButton>Submit</SubmitButton>
|
|
359
|
+
</Form>
|
|
360
|
+
</FormProvider>
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
test("returns the action's data", async () => {
|
|
365
|
+
const result = await submitAction(
|
|
366
|
+
{ formApi: {} as never, value: { id: 1 } },
|
|
367
|
+
(input: { id: number }) =>
|
|
368
|
+
Promise.resolve<ActionResult<string>>({
|
|
369
|
+
data: `saved ${input.id}`,
|
|
370
|
+
ok: true,
|
|
371
|
+
})
|
|
372
|
+
)
|
|
373
|
+
expect(result).toEqual({ data: "saved 1", ok: true })
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test("without reset, the form stays dirty after the save", async () => {
|
|
377
|
+
const user = userEvent.setup()
|
|
378
|
+
const formRef: { current: unknown } = { current: undefined }
|
|
379
|
+
render(<SavedForm formRef={formRef} />)
|
|
380
|
+
|
|
381
|
+
await user.type(screen.getByLabelText(NAME_LABEL), " Lovelace ")
|
|
382
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
383
|
+
|
|
384
|
+
const form = formRef.current as FormState
|
|
385
|
+
await vi.waitFor(() => expect(form.state.isDefaultValue).toBe(false))
|
|
386
|
+
expect(form.state.isDirty).toBe(true)
|
|
387
|
+
expect(form.state.values.name).toBe("Ada Lovelace ")
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test("reset: true rebases the form on the returned data", async () => {
|
|
391
|
+
const user = userEvent.setup()
|
|
392
|
+
const formRef: { current: unknown } = { current: undefined }
|
|
393
|
+
render(<SavedForm formRef={formRef} reset />)
|
|
394
|
+
|
|
395
|
+
await user.type(screen.getByLabelText(NAME_LABEL), " Lovelace ")
|
|
396
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
397
|
+
|
|
398
|
+
await vi.waitFor(() =>
|
|
399
|
+
expect((formRef.current as FormState).state.values.name).toBe(
|
|
400
|
+
"Ada Lovelace"
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
const form = formRef.current as FormState
|
|
404
|
+
expect(form.state.isDefaultValue).toBe(true)
|
|
405
|
+
expect(form.state.isDirty).toBe(false)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('reset: "values" rebases the form on the submitted values', async () => {
|
|
409
|
+
const user = userEvent.setup()
|
|
410
|
+
const formRef: { current: unknown } = { current: undefined }
|
|
411
|
+
render(<SavedForm formRef={formRef} reset="values" />)
|
|
412
|
+
|
|
413
|
+
await user.type(screen.getByLabelText(NAME_LABEL), " Lovelace")
|
|
414
|
+
await user.click(screen.getByRole("button", SUBMIT))
|
|
415
|
+
|
|
416
|
+
const form = formRef.current as FormState
|
|
417
|
+
await vi.waitFor(() => expect(form.state.isDirty).toBe(false))
|
|
418
|
+
expect(form.state.values.name).toBe("Ada Lovelace")
|
|
419
|
+
expect(form.state.isDefaultValue).toBe(true)
|
|
420
|
+
})
|
|
421
|
+
})
|