Skip to content

Commit 16c07e8

Browse files
Update Storybook stories to match repository patterns:
- Use withReactRouterStubDecorator instead of mock fetcher - Update story structure to match other components - Improve test organization with step function - Add source code example in docs Co-authored-by: Jake Ruesink <jake@lambdacurry.dev>
1 parent 9dcee12 commit 16c07e8

2 files changed

Lines changed: 204 additions & 50 deletions

File tree

apps/docs/src/remix-hook-form/region-select.stories.tsx

Lines changed: 194 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
1-
import { Meta, StoryObj } from '@storybook/react';
2-
import { z } from 'zod';
31
import { zodResolver } from '@hookform/resolvers/zod';
4-
import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
5-
import { Button } from '@lambdacurry/forms/ui/button';
62
import { RegionSelect, USStateSelect, CanadaProvinceSelect } from '@lambdacurry/forms/remix-hook-form';
73
import { US_STATES } from '@lambdacurry/forms/ui/data/us-states';
84
import { CANADA_PROVINCES } from '@lambdacurry/forms/ui/data/canada-provinces';
9-
import { testUSStateSelection, testCanadaProvinceSelection, testFormSubmission, testValidationErrors } from './region-select.test';
10-
11-
// Create a mock fetcher to replace the Remix useFetcher
12-
const createMockFetcher = () => {
13-
return {
14-
Form: ({ children, onSubmit }: { children: React.ReactNode; onSubmit: (e: React.FormEvent) => void }) => (
15-
<form onSubmit={onSubmit}>{children}</form>
16-
),
17-
data: null,
18-
state: 'idle',
19-
submit: () => {},
20-
};
21-
};
5+
import { Button } from '@lambdacurry/forms/ui/button';
6+
import type { Meta, StoryObj } from '@storybook/react-vite';
7+
import { expect, userEvent, within } from '@storybook/test';
8+
import { type ActionFunctionArgs, useFetcher } from 'react-router';
9+
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
10+
import { z } from 'zod';
11+
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
2212

2313
const formSchema = z.object({
2414
state: z.string().min(1, 'Please select a state'),
@@ -28,9 +18,8 @@ const formSchema = z.object({
2818

2919
type FormData = z.infer<typeof formSchema>;
3020

31-
function RegionSelectExample() {
32-
// Replace useFetcher with our mock
33-
const fetcher = createMockFetcher();
21+
const RegionSelectExample = () => {
22+
const fetcher = useFetcher<{ message: string; selectedRegions: Record<string, string> }>();
3423

3524
const methods = useRemixForm<FormData>({
3625
resolver: zodResolver(formSchema),
@@ -39,7 +28,7 @@ function RegionSelectExample() {
3928
province: '',
4029
region: '',
4130
},
42-
fetcher: fetcher as any, // Cast to any to satisfy TypeScript
31+
fetcher,
4332
submitConfig: { action: '/', method: 'post' },
4433
});
4534

@@ -87,67 +76,231 @@ function RegionSelectExample() {
8776
</fetcher.Form>
8877
</RemixFormProvider>
8978
);
90-
}
79+
};
80+
81+
const handleFormSubmission = async (request: Request) => {
82+
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));
83+
84+
if (errors) {
85+
return { errors };
86+
}
87+
88+
return {
89+
message: 'Form submitted successfully',
90+
selectedRegions: {
91+
state: data.state,
92+
province: data.province,
93+
region: data.region
94+
}
95+
};
96+
};
9197

92-
export default {
98+
const meta: Meta<typeof RegionSelect> = {
9399
title: 'RemixHookForm/RegionSelect',
94-
component: RegionSelectExample,
95-
} satisfies Meta<typeof RegionSelectExample>;
100+
component: RegionSelect,
101+
parameters: { layout: 'centered' },
102+
tags: ['autodocs'],
103+
decorators: [
104+
withReactRouterStubDecorator({
105+
routes: [
106+
{
107+
path: '/',
108+
Component: RegionSelectExample,
109+
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
110+
},
111+
],
112+
}),
113+
],
114+
} satisfies Meta<typeof RegionSelect>;
96115

97-
type Story = StoryObj<typeof RegionSelectExample>;
116+
export default meta;
117+
type Story = StoryObj<typeof meta>;
98118

99119
export const Default: Story = {
100-
render: () => <RegionSelectExample />,
101120
parameters: {
102121
docs: {
103122
description: {
104123
story: 'A region select component for selecting US states, Canadian provinces, or custom regions.',
105124
},
125+
source: {
126+
code: `
127+
const formSchema = z.object({
128+
state: z.string().min(1, 'Please select a state'),
129+
province: z.string().min(1, 'Please select a province'),
130+
region: z.string().min(1, 'Please select a region'),
131+
});
132+
133+
const RegionSelectExample = () => {
134+
const fetcher = useFetcher<{ message: string; selectedRegions: Record<string, string> }>();
135+
136+
const methods = useRemixForm<FormData>({
137+
resolver: zodResolver(formSchema),
138+
defaultValues: {
139+
state: '',
140+
province: '',
141+
region: '',
142+
},
143+
fetcher,
144+
submitConfig: { action: '/', method: 'post' },
145+
});
146+
147+
return (
148+
<RemixFormProvider {...methods}>
149+
<fetcher.Form onSubmit={methods.handleSubmit} className="space-y-6">
150+
<div className="space-y-4">
151+
<USStateSelect
152+
name="state"
153+
label="US State"
154+
description="Select a US state"
155+
/>
156+
157+
<CanadaProvinceSelect
158+
name="province"
159+
label="Canadian Province"
160+
description="Select a Canadian province"
161+
/>
162+
163+
<RegionSelect
164+
name="region"
165+
label="Custom Region"
166+
description="Select a custom region"
167+
options={[
168+
...US_STATES.slice(0, 5),
169+
...CANADA_PROVINCES.slice(0, 5),
170+
]}
171+
/>
172+
</div>
173+
174+
<Button type="submit">Submit</Button>
175+
</fetcher.Form>
176+
</RemixFormProvider>
177+
);
178+
};`,
179+
},
106180
},
107181
},
108-
play: async (context) => {
109-
await testValidationErrors(context);
182+
play: async ({ canvasElement, step }) => {
183+
const canvas = within(canvasElement);
184+
185+
await step('Verify initial state', async () => {
186+
// Verify all selects are empty initially
187+
const stateSelect = canvas.getByLabelText('US State');
188+
const provinceSelect = canvas.getByLabelText('Canadian Province');
189+
const regionSelect = canvas.getByLabelText('Custom Region');
190+
191+
expect(stateSelect).toHaveTextContent('Select a state');
192+
expect(provinceSelect).toHaveTextContent('Select a province');
193+
expect(regionSelect).toHaveTextContent('Select a custom region');
194+
195+
// Verify submit button is present
196+
const submitButton = canvas.getByRole('button', { name: 'Submit' });
197+
expect(submitButton).toBeInTheDocument();
198+
});
199+
200+
await step('Test validation errors on invalid submission', async () => {
201+
// Submit form without selecting any options
202+
const submitButton = canvas.getByRole('button', { name: 'Submit' });
203+
await userEvent.click(submitButton);
204+
205+
// Verify validation error messages appear
206+
await expect(canvas.findByText('Please select a state')).resolves.toBeInTheDocument();
207+
await expect(canvas.findByText('Please select a province')).resolves.toBeInTheDocument();
208+
await expect(canvas.findByText('Please select a region')).resolves.toBeInTheDocument();
209+
});
110210
},
111211
};
112212

113-
export const USStateSelectionTest: Story = {
114-
render: () => <RegionSelectExample />,
213+
export const USStateSelection: Story = {
115214
parameters: {
116215
docs: {
117216
description: {
118217
story: 'Test selecting a US state from the dropdown.',
119218
},
120219
},
121220
},
122-
play: async (context) => {
123-
await testUSStateSelection(context);
221+
play: async ({ canvasElement, step }) => {
222+
const canvas = within(canvasElement);
223+
224+
await step('Select a US state', async () => {
225+
// Find and click the US state dropdown
226+
const stateSelect = canvas.getByLabelText('US State');
227+
await userEvent.click(stateSelect);
228+
229+
// Wait for dropdown to open and select California
230+
const californiaOption = await canvas.findByText('California');
231+
await userEvent.click(californiaOption);
232+
233+
// Verify the selection
234+
expect(stateSelect).toHaveTextContent('California');
235+
});
124236
},
125237
};
126238

127-
export const CanadaProvinceSelectionTest: Story = {
128-
render: () => <RegionSelectExample />,
239+
export const CanadaProvinceSelection: Story = {
129240
parameters: {
130241
docs: {
131242
description: {
132243
story: 'Test selecting a Canadian province from the dropdown.',
133244
},
134245
},
135246
},
136-
play: async (context) => {
137-
await testCanadaProvinceSelection(context);
247+
play: async ({ canvasElement, step }) => {
248+
const canvas = within(canvasElement);
249+
250+
await step('Select a Canadian province', async () => {
251+
// Find and click the Canada province dropdown
252+
const provinceSelect = canvas.getByLabelText('Canadian Province');
253+
await userEvent.click(provinceSelect);
254+
255+
// Wait for dropdown to open and select Ontario
256+
const ontarioOption = await canvas.findByText('Ontario');
257+
await userEvent.click(ontarioOption);
258+
259+
// Verify the selection
260+
expect(provinceSelect).toHaveTextContent('Ontario');
261+
});
138262
},
139263
};
140264

141-
export const FormSubmissionTest: Story = {
142-
render: () => <RegionSelectExample />,
265+
export const FormSubmission: Story = {
143266
parameters: {
144267
docs: {
145268
description: {
146269
story: 'Test form submission with selected regions.',
147270
},
148271
},
149272
},
150-
play: async (context) => {
151-
await testFormSubmission(context);
273+
play: async ({ canvasElement, step }) => {
274+
const canvas = within(canvasElement);
275+
276+
await step('Select all regions', async () => {
277+
// Select a state
278+
const stateSelect = canvas.getByLabelText('US State');
279+
await userEvent.click(stateSelect);
280+
const californiaOption = await canvas.findByText('California');
281+
await userEvent.click(californiaOption);
282+
283+
// Select a province
284+
const provinceSelect = canvas.getByLabelText('Canadian Province');
285+
await userEvent.click(provinceSelect);
286+
const ontarioOption = await canvas.findByText('Ontario');
287+
await userEvent.click(ontarioOption);
288+
289+
// Select a custom region
290+
const regionSelect = canvas.getByLabelText('Custom Region');
291+
await userEvent.click(regionSelect);
292+
const customOption = await canvas.findByText('New York');
293+
await userEvent.click(customOption);
294+
});
295+
296+
await step('Submit the form', async () => {
297+
// Submit the form
298+
const submitButton = canvas.getByRole('button', { name: 'Submit' });
299+
await userEvent.click(submitButton);
300+
301+
// Verify the submission result
302+
await expect(canvas.findByText('Selected regions:')).resolves.toBeInTheDocument();
303+
});
152304
},
153305
};
306+

apps/docs/src/remix-hook-form/region-select.test.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const testUSStateSelection = async ({ canvasElement }: StoryContext) => {
1111
await userEvent.click(stateDropdown);
1212

1313
// Select a state (e.g., California)
14-
const californiaOption = canvas.getByText('California');
14+
const californiaOption = await canvas.findByText('California');
1515
await userEvent.click(californiaOption);
1616

1717
// Verify the selection
@@ -27,7 +27,7 @@ export const testCanadaProvinceSelection = async ({ canvasElement }: StoryContex
2727
await userEvent.click(provinceDropdown);
2828

2929
// Select a province (e.g., Ontario)
30-
const ontarioOption = canvas.getByText('Ontario');
30+
const ontarioOption = await canvas.findByText('Ontario');
3131
await userEvent.click(ontarioOption);
3232

3333
// Verify the selection
@@ -41,27 +41,27 @@ export const testFormSubmission = async ({ canvasElement }: StoryContext) => {
4141
// Select a state
4242
const stateDropdown = canvas.getByLabelText('US State');
4343
await userEvent.click(stateDropdown);
44-
const californiaOption = canvas.getByText('California');
44+
const californiaOption = await canvas.findByText('California');
4545
await userEvent.click(californiaOption);
4646

4747
// Select a province
4848
const provinceDropdown = canvas.getByLabelText('Canadian Province');
4949
await userEvent.click(provinceDropdown);
50-
const ontarioOption = canvas.getByText('Ontario');
50+
const ontarioOption = await canvas.findByText('Ontario');
5151
await userEvent.click(ontarioOption);
5252

5353
// Select a custom region
5454
const regionDropdown = canvas.getByLabelText('Custom Region');
5555
await userEvent.click(regionDropdown);
56-
const customOption = canvas.getByText('New York');
56+
const customOption = await canvas.findByText('New York');
5757
await userEvent.click(customOption);
5858

5959
// Submit the form
6060
const submitButton = canvas.getByRole('button', { name: 'Submit' });
6161
await userEvent.click(submitButton);
6262

6363
// Verify the submission (mock response would be shown)
64-
await expect(await canvas.findByText('Selected regions:')).toBeInTheDocument();
64+
await expect(canvas.findByText('Selected regions:')).resolves.toBeInTheDocument();
6565
};
6666

6767
// Test validation errors
@@ -73,7 +73,8 @@ export const testValidationErrors = async ({ canvasElement }: StoryContext) => {
7373
await userEvent.click(submitButton);
7474

7575
// Verify error messages
76-
await expect(await canvas.findByText('Please select a state')).toBeInTheDocument();
77-
await expect(await canvas.findByText('Please select a province')).toBeInTheDocument();
78-
await expect(await canvas.findByText('Please select a region')).toBeInTheDocument();
76+
await expect(canvas.findByText('Please select a state')).resolves.toBeInTheDocument();
77+
await expect(canvas.findByText('Please select a province')).resolves.toBeInTheDocument();
78+
await expect(canvas.findByText('Please select a region')).resolves.toBeInTheDocument();
7979
};
80+

0 commit comments

Comments
 (0)