Skip to content

Commit 1fd0f26

Browse files
committed
test(react): query textarea by shadow and add validation tests
1 parent 6c7d3fb commit 1fd0f26

File tree

2 files changed

+163
-82
lines changed

2 files changed

+163
-82
lines changed

packages/react/test/base/src/pages/Inputs.tsx

Lines changed: 132 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,41 @@ const Inputs: React.FC<InputsProps> = () => {
6666
const [segment, setSegment] = useState('dogs');
6767
const [select, setSelect] = useState('apples');
6868

69+
const [touched, setTouched] = useState({
70+
input: false,
71+
inputOtp: false,
72+
textarea: false,
73+
});
74+
75+
const getValidationClasses = (fieldName: keyof typeof touched, value: string | number | null | undefined) => {
76+
const isTouched = touched[fieldName];
77+
let isValid = false;
78+
79+
// Handle ion-input-otp which has multiple inputs
80+
if (fieldName === 'inputOtp') {
81+
// input-otp needs to check if all inputs are filled
82+
// (value length equals component length)
83+
const valueStr = String(value || '');
84+
isValid = valueStr.length === 4;
85+
} else {
86+
const isEmpty = value === '' || value === null || value === undefined;
87+
isValid = !isEmpty;
88+
}
89+
90+
// Always return validation classes
91+
// ion-touched is only added on blur
92+
const classes: string[] = [];
93+
if (isTouched) {
94+
classes.push('ion-touched');
95+
}
96+
if (isValid) {
97+
classes.push('ion-valid');
98+
} else {
99+
classes.push('ion-invalid');
100+
}
101+
return classes.join(' ');
102+
};
103+
69104
const reset = () => {
70105
setCheckbox(false);
71106
setToggle(false);
@@ -78,6 +113,11 @@ const Inputs: React.FC<InputsProps> = () => {
78113
setRadio('red');
79114
setSegment('dogs');
80115
setSelect('apples');
116+
setTouched({
117+
input: false,
118+
inputOtp: false,
119+
textarea: false,
120+
});
81121
};
82122

83123
const set = () => {
@@ -133,96 +173,107 @@ const Inputs: React.FC<InputsProps> = () => {
133173
</IonToolbar>
134174
</IonHeader>
135175

136-
<IonItem>
137-
<IonCheckbox
138-
checked={checkbox}
139-
onIonChange={(e: IonCheckboxCustomEvent<CheckboxChangeEventDetail>) => setCheckbox(e.detail.checked)}
140-
>
141-
Checkbox
142-
</IonCheckbox>
143-
</IonItem>
144-
145-
<IonItem>
146-
<IonToggle
147-
checked={toggle}
148-
onIonChange={(e: IonToggleCustomEvent<ToggleChangeEventDetail>) => setToggle(e.detail.checked)}
149-
>
150-
Toggle
151-
</IonToggle>
152-
</IonItem>
153-
154-
<IonItem>
155-
<IonInput
156-
value={input}
157-
onIonInput={(e: IonInputCustomEvent<InputInputEventDetail>) => setInput(e.detail.value!)}
158-
label="Input"
159-
></IonInput>
160-
</IonItem>
161-
162-
<IonItem>
163-
<IonInputOtp
164-
value={inputOtp}
165-
onIonInput={(e: IonInputOtpCustomEvent<InputOtpInputEventDetail>) => setInputOtp(e.detail.value ?? '')}
166-
></IonInputOtp>
167-
</IonItem>
168-
169-
<IonItem>
170-
<IonRange
171-
label="Range"
172-
dualKnobs={true}
173-
min={0}
174-
max={100}
175-
value={range}
176-
onIonChange={(e: IonRangeCustomEvent<RangeChangeEventDetail>) => setRange(e.detail.value as { lower: number; upper: number })}
177-
></IonRange>
178-
</IonItem>
179-
180-
<IonItem>
181-
<IonTextarea
182-
value={textarea}
183-
onIonInput={(e: IonTextareaCustomEvent<TextareaInputEventDetail>) => setTextarea(e.detail.value!)}
184-
label="Textarea"
185-
></IonTextarea>
186-
</IonItem>
187-
188-
<IonItem>
189-
<IonLabel>Datetime</IonLabel>
190-
<IonDatetime
191-
value={datetime}
192-
onIonChange={(e: IonDatetimeCustomEvent<DatetimeChangeEventDetail>) => {
193-
const value = e.detail.value;
194-
if (typeof value === 'string') {
195-
setDatetime(value);
196-
}
197-
}}
198-
></IonDatetime>
199-
</IonItem>
176+
<form>
177+
<IonItem>
178+
<IonCheckbox
179+
checked={checkbox}
180+
onIonChange={(e: IonCheckboxCustomEvent<CheckboxChangeEventDetail>) => setCheckbox(e.detail.checked)}
181+
>
182+
Checkbox
183+
</IonCheckbox>
184+
</IonItem>
185+
186+
<IonItem>
187+
<IonToggle
188+
checked={toggle}
189+
onIonChange={(e: IonToggleCustomEvent<ToggleChangeEventDetail>) => setToggle(e.detail.checked)}
190+
>
191+
Toggle
192+
</IonToggle>
193+
</IonItem>
194+
195+
<IonItem>
196+
<IonInput
197+
value={input}
198+
onIonInput={(e: IonInputCustomEvent<InputInputEventDetail>) => setInput(e.detail.value!)}
199+
onIonBlur={() => setTouched(prev => ({ ...prev, input: true }))}
200+
className={getValidationClasses('input', input)}
201+
label="Input"
202+
required
203+
></IonInput>
204+
</IonItem>
205+
206+
<IonItem>
207+
<IonInputOtp
208+
value={inputOtp}
209+
onIonInput={(e: IonInputOtpCustomEvent<InputOtpInputEventDetail>) => setInputOtp(e.detail.value ?? '')}
210+
onIonBlur={() => setTouched(prev => ({ ...prev, inputOtp: true }))}
211+
className={getValidationClasses('inputOtp', inputOtp)}
212+
required
213+
></IonInputOtp>
214+
</IonItem>
200215

201-
<IonRadioGroup
202-
value={radio}
203-
onIonChange={(e: IonRadioGroupCustomEvent<RadioGroupChangeEventDetail>) => setRadio(e.detail.value)}
204-
>
205216
<IonItem>
206-
<IonRadio value="red">Red</IonRadio>
217+
<IonRange
218+
label="Range"
219+
dualKnobs={true}
220+
min={0}
221+
max={100}
222+
value={range}
223+
onIonChange={(e: IonRangeCustomEvent<RangeChangeEventDetail>) => setRange(e.detail.value as { lower: number; upper: number })}
224+
></IonRange>
207225
</IonItem>
226+
208227
<IonItem>
209-
<IonRadio value="green">Green</IonRadio>
228+
<IonTextarea
229+
value={textarea}
230+
onIonInput={(e: IonTextareaCustomEvent<TextareaInputEventDetail>) => setTextarea(e.detail.value!)}
231+
onIonBlur={() => setTouched(prev => ({ ...prev, textarea: true }))}
232+
className={getValidationClasses('textarea', textarea)}
233+
label="Textarea"
234+
required
235+
></IonTextarea>
210236
</IonItem>
237+
211238
<IonItem>
212-
<IonRadio value="blue">Blue</IonRadio>
239+
<IonLabel>Datetime</IonLabel>
240+
<IonDatetime
241+
value={datetime}
242+
onIonChange={(e: IonDatetimeCustomEvent<DatetimeChangeEventDetail>) => {
243+
const value = e.detail.value;
244+
if (typeof value === 'string') {
245+
setDatetime(value);
246+
}
247+
}}
248+
></IonDatetime>
213249
</IonItem>
214-
</IonRadioGroup>
215250

216-
<IonItem>
217-
<IonSelect
218-
value={select}
219-
onIonChange={(e: IonSelectCustomEvent<SelectChangeEventDetail<any>>) => setSelect(e.detail.value)}
220-
label="Select"
251+
<IonRadioGroup
252+
value={radio}
253+
onIonChange={(e: IonRadioGroupCustomEvent<RadioGroupChangeEventDetail>) => setRadio(e.detail.value)}
221254
>
222-
<IonSelectOption value="apples">Apples</IonSelectOption>
223-
<IonSelectOption value="bananas">Bananas</IonSelectOption>
224-
</IonSelect>
225-
</IonItem>
255+
<IonItem>
256+
<IonRadio value="red">Red</IonRadio>
257+
</IonItem>
258+
<IonItem>
259+
<IonRadio value="green">Green</IonRadio>
260+
</IonItem>
261+
<IonItem>
262+
<IonRadio value="blue">Blue</IonRadio>
263+
</IonItem>
264+
</IonRadioGroup>
265+
266+
<IonItem>
267+
<IonSelect
268+
value={select}
269+
onIonChange={(e: IonSelectCustomEvent<SelectChangeEventDetail<any>>) => setSelect(e.detail.value)}
270+
label="Select"
271+
>
272+
<IonSelectOption value="apples">Apples</IonSelectOption>
273+
<IonSelectOption value="bananas">Bananas</IonSelectOption>
274+
</IonSelect>
275+
</IonItem>
276+
</form>
226277

227278
<div className="ion-padding">
228279
Checkbox: {checkbox.toString()}<br />

packages/react/test/base/tests/e2e/specs/components/inputs.cy.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,39 @@ describe('Inputs', () => {
6767
});
6868

6969
it('typing into textarea should update ref', () => {
70-
cy.get('ion-textarea textarea').type('Hello Textarea', { scrollBehavior: false });
70+
cy.get('ion-textarea').shadow().find('textarea').type('Hello Textarea', { scrollBehavior: false });
7171

7272
cy.get('#textarea-ref').should('have.text', 'Hello Textarea');
7373
});
7474
});
75+
76+
describe('validation', () => {
77+
it('should show invalid state for required inputs when empty and touched', () => {
78+
cy.get('ion-input input').focus().blur();
79+
cy.get('ion-input').should('have.class', 'ion-invalid');
80+
81+
cy.get('ion-textarea').shadow().find('textarea').focus().blur();
82+
cy.get('ion-textarea').should('have.class', 'ion-invalid');
83+
84+
cy.get('ion-input-otp input').first().focus().blur();
85+
cy.get('ion-input-otp').should('have.class', 'ion-invalid');
86+
});
87+
88+
it('should show invalid state for required input-otp when partially filled', () => {
89+
cy.get('ion-input-otp input').first().focus().blur();
90+
cy.get('ion-input-otp input').eq(0).type('12', { scrollBehavior: false });
91+
cy.get('ion-input-otp').should('have.class', 'ion-invalid');
92+
});
93+
94+
it('should show valid state for required inputs when filled', () => {
95+
cy.get('ion-input input').type('Test value', { scrollBehavior: false });
96+
cy.get('ion-input').should('have.class', 'ion-valid');
97+
98+
cy.get('ion-textarea').shadow().find('textarea').type('Test value', { scrollBehavior: false });
99+
cy.get('ion-textarea').should('have.class', 'ion-valid');
100+
101+
cy.get('ion-input-otp input').eq(0).type('1234', { scrollBehavior: false });
102+
cy.get('ion-input-otp').should('have.class', 'ion-valid');
103+
});
104+
});
75105
})

0 commit comments

Comments
 (0)