Skip to content

Commit d65d36f

Browse files
authored
test(clerk-js): Test cases for parity between PlainInput and FormControl (clerk#2029)
1 parent 5fefc3f commit d65d36f

File tree

3 files changed

+323
-0
lines changed

3 files changed

+323
-0
lines changed

Diff for: .changeset/gorgeous-insects-reply.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Tests for internal PlainInput component.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { describe, it } from '@jest/globals';
2+
import { act, fireEvent, render, waitFor } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
5+
import { useFormControl } from '../../utils';
6+
import { bindCreateFixtures } from '../../utils/test/createFixtures';
7+
import { withCardStateProvider } from '../contexts';
8+
import { Form } from '../Form';
9+
10+
const { createFixtures } = bindCreateFixtures('UserProfile');
11+
const createField = (...params: Parameters<typeof useFormControl>) => {
12+
const MockFieldWrapper = withCardStateProvider((props: Partial<Parameters<typeof Form.PlainInput>[0]>) => {
13+
const field = useFormControl(...params);
14+
15+
return (
16+
<>
17+
{/* @ts-ignore*/}
18+
<Form.PlainInput
19+
{...field.props}
20+
{...props}
21+
/>
22+
<button onClick={() => field.setError('some error')}>set error</button>
23+
</>
24+
);
25+
});
26+
27+
return {
28+
Field: MockFieldWrapper,
29+
};
30+
};
31+
32+
// TODO: Remove this once FormControl is no longer used
33+
const createFormControl = (...params: Parameters<typeof useFormControl>) => {
34+
const MockFieldWrapper = withCardStateProvider((props: Partial<Parameters<typeof Form.PlainInput>[0]>) => {
35+
const field = useFormControl(...params);
36+
37+
return (
38+
<>
39+
{/* @ts-ignore*/}
40+
<Form.Control
41+
{...field.props}
42+
{...props}
43+
/>
44+
<button onClick={() => field.setError('some error')}>set error</button>
45+
</>
46+
);
47+
});
48+
49+
return {
50+
Field: MockFieldWrapper,
51+
};
52+
};
53+
54+
describe('PlainInput', () => {
55+
it('renders the component', async () => {
56+
const { wrapper } = await createFixtures();
57+
const { Field } = createField('firstname', 'init value', {
58+
type: 'text',
59+
label: 'some label',
60+
placeholder: 'some placeholder',
61+
});
62+
63+
const { getByLabelText } = render(<Field />, { wrapper });
64+
expect(getByLabelText('some label')).toHaveValue('init value');
65+
expect(getByLabelText('some label')).toHaveAttribute('name', 'firstname');
66+
expect(getByLabelText('some label')).toHaveAttribute('placeholder', 'some placeholder');
67+
expect(getByLabelText('some label')).toHaveAttribute('type', 'text');
68+
expect(getByLabelText('some label')).toHaveAttribute('id', 'firstname-field');
69+
expect(getByLabelText('some label')).not.toHaveAttribute('disabled');
70+
expect(getByLabelText('some label')).not.toHaveAttribute('required');
71+
expect(getByLabelText('some label')).toHaveAttribute('aria-invalid', 'false');
72+
expect(getByLabelText('some label')).toHaveAttribute('aria-describedby', '');
73+
expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'false');
74+
expect(getByLabelText('some label')).toHaveAttribute('aria-disabled', 'false');
75+
});
76+
77+
it('disabled', async () => {
78+
const { wrapper } = await createFixtures();
79+
const { Field } = createField('firstname', 'init value', {
80+
type: 'text',
81+
label: 'some label',
82+
placeholder: 'some placeholder',
83+
});
84+
85+
const { getByLabelText } = render(<Field isDisabled />, { wrapper });
86+
expect(getByLabelText('some label')).toHaveValue('init value');
87+
expect(getByLabelText('some label')).toHaveAttribute('disabled');
88+
expect(getByLabelText('some label')).toHaveAttribute('aria-disabled', 'true');
89+
});
90+
91+
it('required', async () => {
92+
const { wrapper } = await createFixtures();
93+
const { Field } = createField('firstname', 'init value', {
94+
type: 'text',
95+
label: 'some label',
96+
placeholder: 'some placeholder',
97+
});
98+
99+
const { getByLabelText, queryByText } = render(<Field isRequired />, { wrapper });
100+
expect(getByLabelText('some label')).toHaveValue('init value');
101+
expect(getByLabelText('some label')).toHaveAttribute('required');
102+
expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'true');
103+
expect(queryByText(/optional/i)).not.toBeInTheDocument();
104+
});
105+
106+
it('optional', async () => {
107+
const { wrapper } = await createFixtures();
108+
const { Field } = createField('firstname', 'init value', {
109+
type: 'text',
110+
label: 'some label',
111+
placeholder: 'some placeholder',
112+
});
113+
114+
const { getByLabelText, getByText } = render(<Field isOptional />, { wrapper });
115+
expect(getByLabelText('some label')).not.toHaveAttribute('required');
116+
expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'false');
117+
expect(getByText(/optional/i)).toBeInTheDocument();
118+
});
119+
120+
it('with icon', async () => {
121+
const { wrapper } = await createFixtures();
122+
const { Field } = createField('firstname', 'init value', {
123+
type: 'text',
124+
label: 'some label',
125+
placeholder: 'some placeholder',
126+
});
127+
128+
const Icon = () => <img alt='this is an icon' />;
129+
130+
const { getByAltText } = render(<Field icon={Icon} />, { wrapper });
131+
expect(getByAltText(/this is an icon/i)).toBeInTheDocument();
132+
});
133+
134+
it('with action label', async () => {
135+
const { wrapper } = await createFixtures();
136+
const { Field } = createField('firstname', 'init value', {
137+
type: 'text',
138+
label: 'some label',
139+
placeholder: 'some placeholder',
140+
});
141+
142+
const { getByRole } = render(<Field actionLabel={'take action'} />, { wrapper });
143+
expect(getByRole('link', { name: /take action/i })).toBeInTheDocument();
144+
expect(getByRole('link', { name: /take action/i })).not.toHaveAttribute('rel');
145+
expect(getByRole('link', { name: /take action/i })).not.toHaveAttribute('target');
146+
expect(getByRole('link', { name: /take action/i })).toHaveAttribute('href', '');
147+
});
148+
149+
it('with error', async () => {
150+
const { wrapper } = await createFixtures();
151+
const { Field } = createField('firstname', 'init value', {
152+
type: 'text',
153+
label: 'some label',
154+
placeholder: 'some placeholder',
155+
});
156+
157+
const { getByRole, getByLabelText, getByText } = render(<Field />, { wrapper });
158+
159+
await act(() => userEvent.click(getByRole('button', { name: /set error/i })));
160+
161+
await waitFor(() => {
162+
expect(getByLabelText('some label')).toHaveAttribute('aria-invalid', 'true');
163+
expect(getByLabelText('some label')).toHaveAttribute('aria-describedby', 'error-firstname');
164+
expect(getByText('some error')).toBeInTheDocument();
165+
});
166+
});
167+
168+
it('with info', async () => {
169+
const { wrapper } = await createFixtures();
170+
const { Field } = createField('firstname', 'init value', {
171+
type: 'text',
172+
label: 'some label',
173+
placeholder: 'some placeholder',
174+
infoText: 'some info',
175+
});
176+
177+
const { getByLabelText, getByText } = render(<Field actionLabel={'take action'} />, { wrapper });
178+
await act(() => fireEvent.focus(getByLabelText('some label')));
179+
await waitFor(() => {
180+
expect(getByText('some info')).toBeInTheDocument();
181+
});
182+
});
183+
});
184+
185+
/**
186+
* This tests ensure that the deprecated FormControl and PlainInput continue to behave the same and nothing broke during the refactoring.
187+
*/
188+
describe('Form control as text', () => {
189+
it('renders the component', async () => {
190+
const { wrapper } = await createFixtures();
191+
const { Field } = createFormControl('firstname', 'init value', {
192+
type: 'text',
193+
label: 'some label',
194+
placeholder: 'some placeholder',
195+
});
196+
197+
const { getByLabelText } = render(<Field />, { wrapper });
198+
expect(getByLabelText('some label')).toHaveValue('init value');
199+
expect(getByLabelText('some label')).toHaveAttribute('name', 'firstname');
200+
expect(getByLabelText('some label')).toHaveAttribute('placeholder', 'some placeholder');
201+
expect(getByLabelText('some label')).toHaveAttribute('type', 'text');
202+
expect(getByLabelText('some label')).toHaveAttribute('id', 'firstname-field');
203+
expect(getByLabelText('some label')).not.toHaveAttribute('disabled');
204+
expect(getByLabelText('some label')).not.toHaveAttribute('required');
205+
expect(getByLabelText('some label')).toHaveAttribute('aria-invalid', 'false');
206+
expect(getByLabelText('some label')).toHaveAttribute('aria-describedby', '');
207+
expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'false');
208+
expect(getByLabelText('some label')).toHaveAttribute('aria-disabled', 'false');
209+
});
210+
211+
it('disabled', async () => {
212+
const { wrapper } = await createFixtures();
213+
const { Field } = createFormControl('firstname', 'init value', {
214+
type: 'text',
215+
label: 'some label',
216+
placeholder: 'some placeholder',
217+
});
218+
219+
const { getByLabelText } = render(<Field isDisabled />, { wrapper });
220+
expect(getByLabelText('some label')).toHaveValue('init value');
221+
expect(getByLabelText('some label')).toHaveAttribute('disabled');
222+
expect(getByLabelText('some label')).toHaveAttribute('aria-disabled', 'true');
223+
});
224+
225+
it('required', async () => {
226+
const { wrapper } = await createFixtures();
227+
const { Field } = createFormControl('firstname', 'init value', {
228+
type: 'text',
229+
label: 'some label',
230+
placeholder: 'some placeholder',
231+
});
232+
233+
const { getByLabelText, queryByText } = render(<Field isRequired />, { wrapper });
234+
expect(getByLabelText('some label')).toHaveValue('init value');
235+
expect(getByLabelText('some label')).toHaveAttribute('required');
236+
expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'true');
237+
expect(queryByText(/optional/i)).not.toBeInTheDocument();
238+
});
239+
240+
it('optional', async () => {
241+
const { wrapper } = await createFixtures();
242+
const { Field } = createFormControl('firstname', 'init value', {
243+
type: 'text',
244+
label: 'some label',
245+
placeholder: 'some placeholder',
246+
});
247+
248+
const { getByLabelText, getByText } = render(<Field isOptional />, { wrapper });
249+
expect(getByLabelText('some label')).not.toHaveAttribute('required');
250+
expect(getByLabelText('some label')).toHaveAttribute('aria-required', 'false');
251+
expect(getByText(/optional/i)).toBeInTheDocument();
252+
});
253+
254+
it('with icon', async () => {
255+
const { wrapper } = await createFixtures();
256+
const { Field } = createFormControl('firstname', 'init value', {
257+
type: 'text',
258+
label: 'some label',
259+
placeholder: 'some placeholder',
260+
});
261+
262+
const Icon = () => <img alt='this is an icon' />;
263+
264+
const { getByAltText } = render(<Field icon={Icon} />, { wrapper });
265+
expect(getByAltText(/this is an icon/i)).toBeInTheDocument();
266+
});
267+
268+
it('with action label', async () => {
269+
const { wrapper } = await createFixtures();
270+
const { Field } = createFormControl('firstname', 'init value', {
271+
type: 'text',
272+
label: 'some label',
273+
placeholder: 'some placeholder',
274+
});
275+
276+
const { getByRole } = render(<Field actionLabel={'take action'} />, { wrapper });
277+
expect(getByRole('link', { name: /take action/i })).toBeInTheDocument();
278+
expect(getByRole('link', { name: /take action/i })).not.toHaveAttribute('rel');
279+
expect(getByRole('link', { name: /take action/i })).not.toHaveAttribute('target');
280+
expect(getByRole('link', { name: /take action/i })).toHaveAttribute('href', '');
281+
});
282+
283+
it('with error', async () => {
284+
const { wrapper } = await createFixtures();
285+
const { Field } = createFormControl('firstname', 'init value', {
286+
type: 'text',
287+
label: 'some label',
288+
placeholder: 'some placeholder',
289+
});
290+
291+
const { getByRole, getByLabelText, getByText } = render(<Field />, { wrapper });
292+
293+
await act(() => userEvent.click(getByRole('button', { name: /set error/i })));
294+
295+
await waitFor(() => {
296+
expect(getByLabelText('some label')).toHaveAttribute('aria-invalid', 'true');
297+
expect(getByLabelText('some label')).toHaveAttribute('aria-describedby', 'error-firstname');
298+
expect(getByText('some error')).toBeInTheDocument();
299+
});
300+
});
301+
302+
it('with info', async () => {
303+
const { wrapper } = await createFixtures();
304+
const { Field } = createFormControl('firstname', 'init value', {
305+
type: 'text',
306+
label: 'some label',
307+
placeholder: 'some placeholder',
308+
infoText: 'some info',
309+
});
310+
311+
const { getByLabelText, getByText } = render(<Field actionLabel={'take action'} />, { wrapper });
312+
await act(() => fireEvent.focus(getByLabelText('some label')));
313+
await waitFor(() => {
314+
expect(getByText('some info')).toBeInTheDocument();
315+
});
316+
});
317+
});

Diff for: packages/clerk-js/src/ui/primitives/hooks/useFormControl.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export const sanitizeInputProps = (
190190
fieldId,
191191
label,
192192
clearFeedback,
193+
infoText,
193194
...inputProps
194195
} = obj;
195196
/* eslint-enable */

0 commit comments

Comments
 (0)