1- import { Meta , StoryObj } from '@storybook/react' ;
2- import { z } from 'zod' ;
31import { zodResolver } from '@hookform/resolvers/zod' ;
4- import { RemixFormProvider , useRemixForm } from 'remix-hook-form' ;
5- import { Button } from '@lambdacurry/forms/ui/button' ;
62import { RegionSelect , USStateSelect , CanadaProvinceSelect } from '@lambdacurry/forms/remix-hook-form' ;
73import { US_STATES } from '@lambdacurry/forms/ui/data/us-states' ;
84import { 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
2313const formSchema = z . object ( {
2414 state : z . string ( ) . min ( 1 , 'Please select a state' ) ,
@@ -28,9 +18,8 @@ const formSchema = z.object({
2818
2919type 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
99119export 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+
0 commit comments