SingleValueFieldArray.test.tsx160 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { render, screen, waitFor } from '@testing-library/react'
3import userEvent from '@testing-library/user-event'
4import { useForm } from 'react-hook-form'
5import { describe, expect, it, vi } from 'vitest'
6import { z } from 'zod'
7
8import { Button, Form } from 'ui'
9
10import { SingleValueFieldArray } from './SingleValueFieldArray'
11
12const valueSchema = z.object({
13 urls: z.array(
14 z.object({
15 value: z.string().min(1, 'URL is required'),
16 })
17 ),
18})
19
20const nameSchema = z.object({
21 domains: z.array(
22 z.object({
23 id: z.string(),
24 domain: z.string().min(1, 'Domain is required'),
25 })
26 ),
27})
28
29type ValueFormValues = z.infer<typeof valueSchema>
30type NameFormValues = z.infer<typeof nameSchema>
31
32const ValueForm = ({
33 defaultValues = { urls: [] },
34 onSubmit = vi.fn(),
35}: {
36 defaultValues?: ValueFormValues
37 onSubmit?: (values: ValueFormValues) => void
38}) => {
39 const form = useForm<ValueFormValues>({
40 resolver: zodResolver(valueSchema),
41 defaultValues,
42 })
43
44 return (
45 <Form {...form}>
46 <form onSubmit={form.handleSubmit(onSubmit)}>
47 <SingleValueFieldArray
48 control={form.control}
49 name="urls"
50 valueFieldName="value"
51 createEmptyRow={() => ({ value: '' })}
52 placeholder="https://example.com/callback"
53 addLabel="Add URL"
54 removeLabel="Remove URL"
55 minimumRows={1}
56 />
57 <Button htmlType="submit">Submit</Button>
58 </form>
59 </Form>
60 )
61}
62
63const NameForm = ({ onSubmit = vi.fn() }: { onSubmit?: (values: NameFormValues) => void }) => {
64 const form = useForm<NameFormValues>({
65 resolver: zodResolver(nameSchema),
66 defaultValues: {
67 domains: [{ id: 'row-1', domain: 'example.com' }],
68 },
69 })
70
71 return (
72 <Form {...form}>
73 <form onSubmit={form.handleSubmit(onSubmit)}>
74 <SingleValueFieldArray
75 control={form.control}
76 name="domains"
77 valueFieldName="domain"
78 createEmptyRow={() => ({ id: crypto.randomUUID(), domain: '' })}
79 placeholder="example.com"
80 addLabel="Add domain"
81 removeLabel="Remove domain"
82 minimumRows={1}
83 />
84 <Button htmlType="submit">Submit</Button>
85 </form>
86 </Form>
87 )
88}
89
90describe('SingleValueFieldArray', () => {
91 it('appends an empty row', async () => {
92 const user = userEvent.setup()
93
94 render(<ValueForm defaultValues={{ urls: [{ value: '' }] }} />)
95
96 await user.click(screen.getByRole('button', { name: 'Add URL' }))
97
98 expect(screen.getAllByPlaceholderText('https://example.com/callback')).toHaveLength(2)
99 })
100
101 it('removes a row', async () => {
102 const user = userEvent.setup()
103
104 render(
105 <ValueForm defaultValues={{ urls: [{ value: 'https://example.com' }, { value: '' }] }} />
106 )
107
108 expect(screen.getAllByPlaceholderText('https://example.com/callback')).toHaveLength(2)
109
110 await user.click(screen.getAllByRole('button', { name: 'Remove URL' })[1])
111
112 expect(screen.getAllByPlaceholderText('https://example.com/callback')).toHaveLength(1)
113 })
114
115 it('renders and submits value field names', async () => {
116 const user = userEvent.setup()
117 const onSubmit = vi.fn()
118
119 render(<ValueForm defaultValues={{ urls: [{ value: '' }] }} onSubmit={onSubmit} />)
120
121 await user.type(
122 screen.getByPlaceholderText('https://example.com/callback'),
123 'https://example.com/auth'
124 )
125 await user.click(screen.getByRole('button', { name: 'Submit' }))
126
127 await waitFor(() =>
128 expect(onSubmit).toHaveBeenCalledWith(
129 { urls: [{ value: 'https://example.com/auth' }] },
130 expect.anything()
131 )
132 )
133 })
134
135 it('renders and submits custom field names while preserving row ids', async () => {
136 const user = userEvent.setup()
137 const onSubmit = vi.fn()
138
139 render(<NameForm onSubmit={onSubmit} />)
140
141 await user.click(screen.getByRole('button', { name: 'Submit' }))
142
143 await waitFor(() =>
144 expect(onSubmit).toHaveBeenCalledWith(
145 { domains: [{ id: 'row-1', domain: 'example.com' }] },
146 expect.anything()
147 )
148 )
149 })
150
151 it('shows RHF field errors through FormMessage', async () => {
152 const user = userEvent.setup()
153
154 render(<ValueForm defaultValues={{ urls: [{ value: '' }] }} />)
155
156 await user.click(screen.getByRole('button', { name: 'Submit' }))
157
158 expect(await screen.findByText('URL is required')).toBeInTheDocument()
159 })
160})