-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpassword-field.stories.tsx
More file actions
231 lines (191 loc) · 8 KB
/
Copy pathpassword-field.stories.tsx
File metadata and controls
231 lines (191 loc) · 8 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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import { zodResolver } from '@hookform/resolvers/zod';
import { PasswordField } from '@lambdacurry/forms/remix-hook-form/password-field';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { expect, userEvent } from '@storybook/test';
import { useRef } from 'react';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
const formSchema = z
.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string().min(1, 'Please confirm your password'),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type FormData = z.infer<typeof formSchema>;
const INITIAL_PASSWORD = 'test123456';
const WEAK_PASSWORD = '123';
const WEAK_PASSWORD_ERROR = 'Password must be at least 8 characters';
const MISMATCH_PASSWORD_ERROR = "Passwords don't match";
const CreateAccountForm = () => {
const fetcher = useFetcher<{ message: string; success: boolean }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
password: INITIAL_PASSWORD,
confirmPassword: INITIAL_PASSWORD,
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});
const ref = useRef<HTMLInputElement>(null);
return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-6 max-w-md">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Create Account</h2>
<p className="text-gray-600 mt-2">Enter your password details to get started</p>
</div>
<PasswordField
name="password"
label="Password"
description="Must be at least 8 characters long"
placeholder="Enter your password"
/>
<PasswordField
name="confirmPassword"
label="Confirm Password"
description="Re-enter your password to confirm"
placeholder="Confirm your password"
/>
<div className="flex gap-2 items-end">
<PasswordField name="refExample" label="Ref Example" placeholder="This field has a ref" ref={ref} />
<Button type="button" onClick={() => ref.current?.focus()}>
Focus
</Button>
</div>
<Button type="submit" className="w-full mt-6">
Create Account
</Button>
{fetcher.data?.message && (
<p className={`mt-2 text-center ${fetcher.data.success ? 'text-green-600' : 'text-red-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 };
}
// Simulate account creation - data is validated at this point
console.log('Creating account with password length:', data.password.length);
return {
message: 'Account created successfully! Welcome aboard!',
success: true,
};
};
const meta: Meta<typeof PasswordField> = {
title: 'RemixHookForm/PasswordField',
component: PasswordField,
parameters: { layout: 'centered' },
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// Test scenarios
const testDefaultValues = ({ canvas }: StoryContext) => {
const passwordInput = canvas.getByLabelText('Password');
const confirmInput = canvas.getByLabelText('Confirm Password');
expect(passwordInput).toHaveValue(INITIAL_PASSWORD);
expect(confirmInput).toHaveValue(INITIAL_PASSWORD);
};
const testPasswordVisibilityToggle = async ({ canvas }: StoryContext) => {
const passwordInput = canvas.getByLabelText('Password');
// Find the toggle button within the same form item as the password input
const formItem =
passwordInput.closest('[class*="FormItem"], .form-item, [data-testid="form-item"]') ||
passwordInput.parentElement?.parentElement;
const toggleButton = formItem?.querySelector('button[aria-label="Show password"]') as HTMLElement;
// Initially password should be hidden (type="password")
expect(passwordInput).toHaveAttribute('type', 'password');
// Click toggle to show password
await userEvent.click(toggleButton);
expect(passwordInput).toHaveAttribute('type', 'text');
// Find the hide button for the same field
const hideButton = formItem?.querySelector('button[aria-label="Hide password"]') as HTMLElement;
expect(hideButton).toBeInTheDocument();
// Click toggle to hide password again
await userEvent.click(hideButton);
expect(passwordInput).toHaveAttribute('type', 'password');
// Verify show button is back
const showButtonAgain = formItem?.querySelector('button[aria-label="Show password"]') as HTMLElement;
expect(showButtonAgain).toBeInTheDocument();
};
const testWeakPasswordValidation = async ({ canvas }: StoryContext) => {
const passwordInput = canvas.getByLabelText('Password');
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
await userEvent.click(passwordInput);
await userEvent.clear(passwordInput);
await userEvent.type(passwordInput, WEAK_PASSWORD);
await userEvent.click(submitButton);
await expect(await canvas.findByText(WEAK_PASSWORD_ERROR)).toBeInTheDocument();
};
const testPasswordMismatchValidation = async ({ canvas }: StoryContext) => {
const passwordInput = canvas.getByLabelText('Password');
const confirmInput = canvas.getByLabelText('Confirm Password');
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
await userEvent.click(passwordInput);
await userEvent.clear(passwordInput);
await userEvent.type(passwordInput, 'validpassword123');
await userEvent.click(confirmInput);
await userEvent.clear(confirmInput);
await userEvent.type(confirmInput, 'differentpassword123');
await userEvent.click(submitButton);
await expect(await canvas.findByText(MISMATCH_PASSWORD_ERROR)).toBeInTheDocument();
};
const testValidSubmission = async ({ canvas }: StoryContext) => {
const passwordInput = canvas.getByLabelText('Password');
const confirmInput = canvas.getByLabelText('Confirm Password');
const submitButton = canvas.getByRole('button', { name: 'Create Account' });
await userEvent.click(passwordInput);
await userEvent.clear(passwordInput);
await userEvent.type(passwordInput, 'validpassword123');
await userEvent.click(confirmInput);
await userEvent.clear(confirmInput);
await userEvent.type(confirmInput, 'validpassword123');
await userEvent.click(submitButton);
const successMessage = await canvas.findByText('Account created successfully! Welcome aboard!');
expect(successMessage).toBeInTheDocument();
};
const testRefFunctionality = async ({ canvas }: StoryContext) => {
const refInput = canvas.getByLabelText('Ref Example');
const focusButton = canvas.getByRole('button', { name: 'Focus' });
await userEvent.click(focusButton);
expect(refInput).toHaveFocus();
};
// Single story that contains all variants and tests
export const CreateAccountExample: Story = {
play: async (storyContext) => {
testDefaultValues(storyContext);
await testPasswordVisibilityToggle(storyContext);
await testWeakPasswordValidation(storyContext);
await testPasswordMismatchValidation(storyContext);
await testValidSubmission(storyContext);
await testRefFunctionality(storyContext);
},
decorators: [
withReactRouterStubDecorator({
routes: [
{
path: '/',
Component: CreateAccountForm,
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
},
],
}),
],
};