11import type { Meta , StoryObj } from '@storybook/nextjs-vite' ;
2- import { useState } from 'react' ;
3- import { expect , screen , userEvent , waitFor } from 'storybook/test' ;
2+ import { useEffect , useRef , useState } from 'react' ;
3+ import { expect , screen , userEvent , waitFor , within } from 'storybook/test' ;
4+ import Surface from '~/components/layout/Surface' ;
5+ import Heading from '~/components/typography/Heading' ;
6+ import Paragraph from '~/components/typography/Paragraph' ;
47import Field from '~/lib/form/components/Field/Field' ;
58import CheckboxGroupField from '~/lib/form/components/fields/CheckboxGroup' ;
9+ import InputField from '~/lib/form/components/fields/InputField' ;
10+ import RadioGroupField from '~/lib/form/components/fields/RadioGroup' ;
611import useFormStore from '~/lib/form/hooks/useFormStore' ;
712import FormStoreProvider from '~/lib/form/store/formStoreProvider' ;
813
14+ // ---- Simple controlled test ----
15+
916function ControlledCheckboxGroup ( ) {
1017 const [ values , setValues ] = useState < ( string | number ) [ ] > ( [ 'a' , 'b' , 'c' ] ) ;
1118
@@ -27,140 +34,136 @@ function ControlledCheckboxGroup() {
2734 ) ;
2835}
2936
30- const meta : Meta = {
31- title : 'Form/Fields/CheckboxGroup Interaction' ,
32- component : ControlledCheckboxGroup ,
33- } ;
34-
35- export default meta ;
36- type Story = StoryObj ;
37+ // ---- Wizard step reproduction ----
38+ // Mimics the SiblingsDetailStep exactly: FormStoreProvider, ego-parents
39+ // checkbox group, sibling name + sex fields, sibling shared-parents checkbox group
3740
38- export const ClickCheckbox : Story = {
39- play : async ( ) => {
40- // Find checkbox C and verify it starts checked
41- const checkboxC = await screen . findByRole ( 'checkbox' , { name : 'Option C ' } ) ;
42- await expect ( checkboxC ) . toHaveAttribute ( 'aria-checked' , 'true' ) ;
41+ const PARENTS = [
42+ { name : 'Mom' , value : '0' } ,
43+ { name : 'Donor 1' , value : '1' } ,
44+ { name : 'Donor 2' , value : '2 ' } ,
45+ ] ;
4346
44- // Click to uncheck
45- await userEvent . click ( checkboxC ) ;
47+ const SEX_OPTIONS = [
48+ { value : 'male' , label : 'Male' } ,
49+ { value : 'female' , label : 'Female' } ,
50+ ] ;
4651
47- // Verify it unchecked
48- await waitFor ( async ( ) => {
49- await expect ( checkboxC ) . toHaveAttribute ( 'aria-checked' , 'false' ) ;
50- } ) ;
51-
52- await waitFor ( async ( ) => {
53- await expect ( screen . getByTestId ( 'result' ) ) . toHaveTextContent ( '["a","b"]' ) ;
54- } ) ;
55- } ,
56- } ;
57-
58- function FormFieldCheckboxGroup ( ) {
52+ function WizardStepReproduction ( ) {
5953 return (
6054 < FormStoreProvider >
61- < FormFieldInner />
55+ < WizardStepInner />
6256 </ FormStoreProvider >
6357 ) ;
6458}
6559
66- function FormFieldInner ( ) {
60+ function WizardStepInner ( ) {
61+ const validateForm = useFormStore ( ( s ) => s . validateForm ) ;
6762 const getFormValues = useFormStore ( ( s ) => s . getFormValues ) ;
68- const values = getFormValues ( ) ;
63+ const [ submitted , setSubmitted ] = useState < string | null > ( null ) ;
64+ const getFormValuesRef = useRef ( getFormValues ) ;
65+ getFormValuesRef . current = getFormValues ;
6966
70- return (
71- < div >
72- < Field
73- name = "choices"
74- label = "Pick options"
75- component = { CheckboxGroupField }
76- options = { [
77- { value : 'a' , label : 'Option A' } ,
78- { value : 'b' , label : 'Option B' } ,
79- { value : 'c' , label : 'Option C' } ,
80- ] }
81- initialValue = { [ 'a' , 'b' , 'c' ] }
82- />
83- < div data-testid = "form-result" >
84- { JSON . stringify ( values . choices ?? 'undefined' ) }
85- </ div >
86- </ div >
87- ) ;
88- }
67+ const handleSubmit = async ( ) => {
68+ const isValid = await validateForm ( ) ;
69+ if ( ! isValid ) return ;
8970
90- export const ClickCheckboxInFormField : Story = {
91- render : ( ) => < FormFieldCheckboxGroup /> ,
92- play : async ( ) => {
93- const checkboxC = await screen . findByRole ( 'checkbox' , { name : 'Option C' } ) ;
94- await expect ( checkboxC ) . toHaveAttribute ( 'aria-checked' , 'true' ) ;
71+ const values = getFormValuesRef . current ( ) ;
72+ const rawEgoParents = values [ 'ego-parents' ] ;
73+ const egoParentIndices = Array . isArray ( rawEgoParents )
74+ ? rawEgoParents . map ( ( v ) => Number ( v ) )
75+ : [ 0 , 1 , 2 ] ;
9576
96- await userEvent . click ( checkboxC ) ;
77+ const rawSharedParents = values [ 'sibling-0-sharedParents' ] ;
78+ const sharedParentIndices = Array . isArray ( rawSharedParents )
79+ ? rawSharedParents . map ( ( v ) => Number ( v ) )
80+ : [ ] ;
9781
98- await waitFor ( async ( ) => {
99- await expect ( checkboxC ) . toHaveAttribute ( 'aria-checked' , 'false' ) ;
100- } ) ;
82+ setSubmitted ( JSON . stringify ( { egoParentIndices, sharedParentIndices } ) ) ;
83+ } ;
10184
102- await waitFor ( async ( ) => {
103- await expect ( screen . getByTestId ( 'form-result' ) ) . toHaveTextContent (
104- '["a","b"]' ,
105- ) ;
106- } ) ;
107- } ,
108- } ;
85+ return (
86+ < div className = "flex flex-col gap-6 p-4" >
87+ < Surface level = { 1 } spacing = "sm" >
88+ < Paragraph >
89+ Since you have multiple parents, please confirm which are specifically
90+ your parents.
91+ </ Paragraph >
92+ < Field
93+ name = "ego-parents"
94+ label = "Which of these parents are YOUR parents?"
95+ data-testid = "ego-parents-checkboxes"
96+ component = { CheckboxGroupField }
97+ options = { PARENTS . map ( ( p ) => ( { value : p . value , label : p . name } ) ) }
98+ initialValue = { PARENTS . map ( ( p ) => p . value ) }
99+ />
100+ </ Surface >
101+
102+ < div className = "flex flex-col gap-3 rounded border p-4" >
103+ < Heading level = "h3" > Sibling 1</ Heading >
104+ < Field
105+ name = "sibling-0-name"
106+ label = "Name"
107+ component = { InputField }
108+ placeholder = "Enter name"
109+ required
110+ />
111+ < Field
112+ name = "sibling-0-sex"
113+ label = "Sex assigned at birth"
114+ component = { RadioGroupField }
115+ options = { SEX_OPTIONS }
116+ required
117+ />
118+ < Field
119+ name = "sibling-0-sharedParents"
120+ label = "Which of your parents are also this sibling's parent?"
121+ component = { CheckboxGroupField }
122+ options = { PARENTS . map ( ( p ) => ( { value : p . value , label : p . name } ) ) }
123+ initialValue = { PARENTS . map ( ( p ) => p . value ) }
124+ />
125+ </ div >
109126
110- function DialogCheckboxGroup ( ) {
111- const [ open , setOpen ] = useState ( true ) ;
127+ < button onClick = { ( ) => void handleSubmit ( ) } > Submit</ button >
112128
113- return (
114- < div >
115- < button onClick = { ( ) => setOpen ( true ) } > Open</ button >
116- { open && (
117- < dialog open style = { { position : 'fixed' , zIndex : 1000 } } >
118- < FormStoreProvider >
119- < DialogCheckboxInner onClose = { ( ) => setOpen ( false ) } />
120- </ FormStoreProvider >
121- </ dialog >
122- ) }
129+ { submitted && < div data-testid = "wizard-result" > { submitted } </ div > }
123130 </ div >
124131 ) ;
125132}
126133
127- function DialogCheckboxInner ( { onClose } : { onClose : ( ) => void } ) {
128- return (
129- < div >
130- < Field
131- name = "choices"
132- label = "Pick options"
133- data-testid = "dialog-checkboxes"
134- component = { CheckboxGroupField }
135- options = { [
136- { value : 'a' , label : 'Option A' } ,
137- { value : 'b' , label : 'Option B' } ,
138- { value : 'c' , label : 'Option C' } ,
139- ] }
140- initialValue = { [ 'a' , 'b' , 'c' ] }
141- />
142- < button onClick = { onClose } > Close</ button >
143- </ div >
144- ) ;
145- }
134+ // ---- Meta ----
135+
136+ const meta : Meta = {
137+ title : 'Form/Fields/CheckboxGroup Interaction' ,
138+ component : ControlledCheckboxGroup ,
139+ } ;
140+
141+ export default meta ;
142+ type Story = StoryObj ;
146143
147- export const ClickCheckboxInDialog : Story = {
148- render : ( ) => < DialogCheckboxGroup /> ,
144+ // ---- Stories ----
145+
146+ export const ClickCheckbox : Story = {
149147 play : async ( ) => {
150- const checkboxC = await screen . findByRole ( 'checkbox' , { name : 'Option C' } ) ;
148+ const checkboxC = await screen . findByRole ( 'checkbox' , {
149+ name : 'Option C' ,
150+ } ) ;
151151 await expect ( checkboxC ) . toHaveAttribute ( 'aria-checked' , 'true' ) ;
152152
153153 await userEvent . click ( checkboxC ) ;
154154
155155 await waitFor ( async ( ) => {
156156 await expect ( checkboxC ) . toHaveAttribute ( 'aria-checked' , 'false' ) ;
157157 } ) ;
158+
159+ await waitFor ( async ( ) => {
160+ await expect ( screen . getByTestId ( 'result' ) ) . toHaveTextContent ( '["a","b"]' ) ;
161+ } ) ;
158162 } ,
159163} ;
160164
161165export const ClickLabel : Story = {
162166 play : async ( ) => {
163- // Find the text label and click it instead of the checkbox button
164167 const labelText = await screen . findByText ( 'Option C' ) ;
165168 await expect (
166169 screen . getByRole ( 'checkbox' , { name : 'Option C' } ) ,
@@ -179,3 +182,45 @@ export const ClickLabel: Story = {
179182 } ) ;
180183 } ,
181184} ;
185+
186+ export const WizardStepRepro : Story = {
187+ render : ( ) => < WizardStepReproduction /> ,
188+ play : async ( ) => {
189+ // 1. Uncheck Donor 2 from ego's parents
190+ const egoContainer = await screen . findByTestId ( 'ego-parents-checkboxes' ) ;
191+ const egoScope = within ( egoContainer ) ;
192+ const donor2Cb = await egoScope . findByRole ( 'checkbox' , {
193+ name : 'Donor 2' ,
194+ } ) ;
195+ await expect ( donor2Cb ) . toHaveAttribute ( 'aria-checked' , 'true' ) ;
196+ await userEvent . click ( donor2Cb ) ;
197+ await waitFor ( async ( ) => {
198+ await expect ( donor2Cb ) . toHaveAttribute ( 'aria-checked' , 'false' ) ;
199+ } ) ;
200+
201+ // 2. Fill sibling name + sex
202+ const nameInput = await screen . findByRole ( 'textbox' ) ;
203+ await userEvent . click ( nameInput ) ;
204+ await userEvent . type ( nameInput , 'Half Sib' ) ;
205+ await userEvent . click ( await screen . findByRole ( 'radio' , { name : 'Female' } ) ) ;
206+
207+ // 3. Uncheck Donor 1 from sibling's shared parents
208+ const donor1Cbs = await screen . findAllByRole ( 'checkbox' , {
209+ name : 'Donor 1' ,
210+ } ) ;
211+ // Index 0 = ego group, index 1 = sibling group
212+ await userEvent . click ( donor1Cbs [ 1 ] ! ) ;
213+ await waitFor ( async ( ) => {
214+ await expect ( donor1Cbs [ 1 ] ) . toHaveAttribute ( 'aria-checked' , 'false' ) ;
215+ } ) ;
216+
217+ // 4. Submit and verify
218+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'Submit' } ) ) ;
219+ await waitFor ( async ( ) => {
220+ const result = screen . getByTestId ( 'wizard-result' ) ;
221+ const data = JSON . parse ( result . textContent ! ) ;
222+ await expect ( data . egoParentIndices ) . toEqual ( [ 0 , 1 ] ) ;
223+ await expect ( data . sharedParentIndices ) . toEqual ( [ 0 , 2 ] ) ;
224+ } ) ;
225+ } ,
226+ } ;
0 commit comments