Skip to content

Commit 391dc5e

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

File tree

2 files changed

+168
-82
lines changed

2 files changed

+168
-82
lines changed

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

Lines changed: 137 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,42 @@ 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+
searchbar: false,
74+
});
75+
76+
const getValidationClasses = (fieldName: keyof typeof touched, value: string | number | null | undefined) => {
77+
const isTouched = touched[fieldName];
78+
let isValid = false;
79+
80+
// Handle ion-input-otp which has multiple inputs
81+
if (fieldName === 'inputOtp') {
82+
// input-otp needs to check if all inputs are filled
83+
// (value length equals component length)
84+
const valueStr = String(value || '');
85+
isValid = valueStr.length === 4;
86+
} else {
87+
const isEmpty = value === '' || value === null || value === undefined;
88+
isValid = !isEmpty;
89+
}
90+
91+
// Always return validation classes
92+
// ion-touched is only added on blur
93+
const classes: string[] = [];
94+
if (isTouched) {
95+
classes.push('ion-touched');
96+
}
97+
if (isValid) {
98+
classes.push('ion-valid');
99+
} else {
100+
classes.push('ion-invalid');
101+
}
102+
return classes.join(' ');
103+
};
104+
69105
const reset = () => {
70106
setCheckbox(false);
71107
setToggle(false);
@@ -78,6 +114,12 @@ const Inputs: React.FC<InputsProps> = () => {
78114
setRadio('red');
79115
setSegment('dogs');
80116
setSelect('apples');
117+
setTouched({
118+
input: false,
119+
inputOtp: false,
120+
textarea: false,
121+
searchbar: false,
122+
});
81123
};
82124

83125
const set = () => {
@@ -122,6 +164,9 @@ const Inputs: React.FC<InputsProps> = () => {
122164
<IonSearchbar
123165
value={searchbar}
124166
onIonInput={(e: IonSearchbarCustomEvent<SearchbarInputEventDetail>) => setSearchbar(e.detail.value!)}
167+
onIonBlur={() => setTouched(prev => ({ ...prev, searchbar: true }))}
168+
className={getValidationClasses('searchbar', searchbar)}
169+
required
125170
></IonSearchbar>
126171
</IonToolbar>
127172
</IonHeader>
@@ -133,96 +178,107 @@ const Inputs: React.FC<InputsProps> = () => {
133178
</IonToolbar>
134179
</IonHeader>
135180

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>
181+
<form>
182+
<IonItem>
183+
<IonCheckbox
184+
checked={checkbox}
185+
onIonChange={(e: IonCheckboxCustomEvent<CheckboxChangeEventDetail>) => setCheckbox(e.detail.checked)}
186+
>
187+
Checkbox
188+
</IonCheckbox>
189+
</IonItem>
190+
191+
<IonItem>
192+
<IonToggle
193+
checked={toggle}
194+
onIonChange={(e: IonToggleCustomEvent<ToggleChangeEventDetail>) => setToggle(e.detail.checked)}
195+
>
196+
Toggle
197+
</IonToggle>
198+
</IonItem>
199+
200+
<IonItem>
201+
<IonInput
202+
value={input}
203+
onIonInput={(e: IonInputCustomEvent<InputInputEventDetail>) => setInput(e.detail.value!)}
204+
onIonBlur={() => setTouched(prev => ({ ...prev, input: true }))}
205+
className={getValidationClasses('input', input)}
206+
label="Input"
207+
required
208+
></IonInput>
209+
</IonItem>
210+
211+
<IonItem>
212+
<IonInputOtp
213+
value={inputOtp}
214+
onIonInput={(e: IonInputOtpCustomEvent<InputOtpInputEventDetail>) => setInputOtp(e.detail.value ?? '')}
215+
onIonBlur={() => setTouched(prev => ({ ...prev, inputOtp: true }))}
216+
className={getValidationClasses('inputOtp', inputOtp)}
217+
required
218+
></IonInputOtp>
219+
</IonItem>
200220

201-
<IonRadioGroup
202-
value={radio}
203-
onIonChange={(e: IonRadioGroupCustomEvent<RadioGroupChangeEventDetail>) => setRadio(e.detail.value)}
204-
>
205221
<IonItem>
206-
<IonRadio value="red">Red</IonRadio>
222+
<IonRange
223+
label="Range"
224+
dualKnobs={true}
225+
min={0}
226+
max={100}
227+
value={range}
228+
onIonChange={(e: IonRangeCustomEvent<RangeChangeEventDetail>) => setRange(e.detail.value as { lower: number; upper: number })}
229+
></IonRange>
207230
</IonItem>
231+
208232
<IonItem>
209-
<IonRadio value="green">Green</IonRadio>
233+
<IonTextarea
234+
value={textarea}
235+
onIonInput={(e: IonTextareaCustomEvent<TextareaInputEventDetail>) => setTextarea(e.detail.value!)}
236+
onIonBlur={() => setTouched(prev => ({ ...prev, textarea: true }))}
237+
className={getValidationClasses('textarea', textarea)}
238+
label="Textarea"
239+
required
240+
></IonTextarea>
210241
</IonItem>
242+
211243
<IonItem>
212-
<IonRadio value="blue">Blue</IonRadio>
244+
<IonLabel>Datetime</IonLabel>
245+
<IonDatetime
246+
value={datetime}
247+
onIonChange={(e: IonDatetimeCustomEvent<DatetimeChangeEventDetail>) => {
248+
const value = e.detail.value;
249+
if (typeof value === 'string') {
250+
setDatetime(value);
251+
}
252+
}}
253+
></IonDatetime>
213254
</IonItem>
214-
</IonRadioGroup>
215255

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

227283
<div className="ion-padding">
228284
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)