-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtext-field.stories.tsx
More file actions
179 lines (150 loc) · 5.78 KB
/
Copy pathtext-field.stories.tsx
File metadata and controls
179 lines (150 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import { zodResolver } from '@hookform/resolvers/zod';
import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { useRef } from 'react';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { expect, userEvent } from 'storybook/test';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
const formSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters'),
price: z.string().min(1, 'Price is required'),
email: z.string().email('Invalid email address'),
measurement: z.string().min(1, 'Measurement is required'),
});
type FormData = z.infer<typeof formSchema>;
const INITIAL_USERNAME = 'test_user';
const USERNAME_TAKEN = 'test_user';
const USERNAME_TAKEN_ERROR = 'Username is already taken';
const ControlledTextFieldExample = () => {
const fetcher = useFetcher<{ message: string; email: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
username: INITIAL_USERNAME,
price: '10.00',
email: 'user@example.com',
measurement: '10',
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});
const ref = useRef<HTMLInputElement>(null);
return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-6">
<TextField name="username" label="Username" description="Enter a unique username" />
<TextField name="price" label="Price" description="Enter the price" prefix="$" />
<TextField name="email" label="Email" description="Enter your email address" suffix="@example.com" />
<TextField
type="number"
name="measurement"
step={0.1}
label="Measurement"
description="Enter a measurement"
prefix="~"
suffix="cm"
/>
<div className="flex gap-2 items-end">
<TextField name="refExample" label="Ref Example" placeholder="Enter something, this has a ref" ref={ref} />
<Button onClick={() => ref.current?.focus()}>Focus the input</Button>
</div>
<Button type="submit" className="mt-4">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
</div>
</fetcher.Form>
</RemixFormProvider>
);
};
const handleFormSubmission = async (request: Request) => {
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));
if (errors) {
return { errors };
}
// Check for taken username
if (data.username === USERNAME_TAKEN) {
return {
errors: {
username: {
message: USERNAME_TAKEN_ERROR,
},
},
};
}
return { message: 'Form submitted successfully', email: data.email };
};
const meta: Meta<typeof TextField> = {
title: 'RemixHookForm/TextField',
component: TextField,
parameters: { layout: 'centered' },
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// Test scenarios
const testDefaultValues = ({ canvas }: StoryContext) => {
const input = canvas.getByLabelText('Username');
expect(input).toHaveValue(INITIAL_USERNAME);
};
const testInvalidSubmission = async ({ canvas }: StoryContext) => {
const input = canvas.getByLabelText('Username');
const submitButton = canvas.getByRole('button', { name: 'Submit' });
// Note: clicking the input before clearing his helpful to make sure it is ready to be cleared
await userEvent.click(input);
await userEvent.clear(input);
await userEvent.type(input, 'ab');
await userEvent.click(submitButton);
// Use findByText instead of getByText to allow for async updates
await expect(await canvas.findByText('Username must be at least 3 characters')).toBeInTheDocument();
};
const testUsernameTaken = async ({ canvas }: StoryContext) => {
const input = canvas.getByLabelText('Username');
const submitButton = canvas.getByRole('button', { name: 'Submit' });
// Note: clicking the input before clearing his helpful to make sure it is ready to be cleared
await userEvent.click(input);
await userEvent.clear(input);
await userEvent.type(input, USERNAME_TAKEN);
await userEvent.click(submitButton);
// wait for response to return
await new Promise((resolve) => setTimeout(resolve, 1000));
await expect(canvas.getByText(USERNAME_TAKEN_ERROR)).toBeInTheDocument();
};
const testValidSubmission = async ({ canvas }: StoryContext) => {
const input = canvas.getByLabelText('Username');
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(input);
await userEvent.clear(input);
await userEvent.type(input, 'valid_username');
await userEvent.click(submitButton);
// Use findByText which waits for the element to appear
const successMessage = await canvas.findByText('Form submitted successfully');
expect(successMessage).toBeInTheDocument();
};
// Single story that contains all variants
export const Examples: Story = {
play: async (storyContext) => {
testDefaultValues(storyContext);
await testInvalidSubmission(storyContext);
await testUsernameTaken(storyContext);
await testValidSubmission(storyContext);
},
decorators: [
withReactRouterStubDecorator({
routes: [
{
path: '/',
Component: ControlledTextFieldExample,
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
},
],
}),
],
};