Skip to content

Commit 6c7d3fb

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

File tree

2 files changed

+132
-97
lines changed

2 files changed

+132
-97
lines changed

packages/vue/test/base/src/views/Inputs.vue

Lines changed: 100 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,19 @@
3838
</ion-item>
3939

4040
<ion-item>
41-
<ion-input v-model="input" label="Input"></ion-input>
41+
<ion-input v-model="input" label="Input" required @ionBlur="handleValidation" @ionInput="handleValidation"></ion-input>
4242
</ion-item>
4343

4444
<ion-item>
45-
<ion-input-otp v-model="inputOtp"></ion-input-otp>
45+
<ion-input-otp v-model="inputOtp" required @ionBlur="handleValidation" @ionInput="handleValidation"></ion-input-otp>
4646
</ion-item>
4747

4848
<ion-item>
4949
<ion-range label="Range" :dual-knobs="true" :min="0" :max="100" slot="end" v-model="range"></ion-range>
5050
</ion-item>
5151

5252
<ion-item>
53-
<ion-textarea label="Textarea" v-model="textarea"></ion-textarea>
53+
<ion-textarea label="Textarea" v-model="textarea" required @ionBlur="handleValidation" @ionInput="handleValidation"></ion-textarea>
5454
</ion-item>
5555

5656
<ion-item>
@@ -99,7 +99,7 @@
9999
</ion-page>
100100
</template>
101101

102-
<script lang="ts">
102+
<script setup lang="ts">
103103
import {
104104
IonBackButton,
105105
IonButton,
@@ -126,101 +126,105 @@ import {
126126
IonToggle,
127127
IonToolbar
128128
} from '@ionic/vue';
129-
import { defineComponent, ref } from 'vue';
130-
131-
export default defineComponent({
132-
components: {
133-
IonBackButton,
134-
IonButton,
135-
IonButtons,
136-
IonCheckbox,
137-
IonContent,
138-
IonDatetime,
139-
IonHeader,
140-
IonInput,
141-
IonInputOtp,
142-
IonItem,
143-
IonLabel,
144-
IonPage,
145-
IonRadio,
146-
IonRadioGroup,
147-
IonRange,
148-
IonSearchbar,
149-
IonSegment,
150-
IonSegmentButton,
151-
IonSelect,
152-
IonSelectOption,
153-
IonTextarea,
154-
IonTitle,
155-
IonToggle,
156-
IonToolbar
157-
},
158-
setup() {
159-
const checkbox = ref(false);
160-
const toggle = ref(false);
161-
const input = ref('');
162-
const inputOtp = ref('');
163-
const range = ref({
164-
lower: 30,
165-
upper: 70
166-
});
167-
const textarea = ref('');
168-
const searchbar = ref('');
169-
const datetime = ref('');
170-
const radio = ref('red');
171-
const segment = ref('dogs');
172-
const select = ref('apples');
173-
174-
const reset = () => {
175-
checkbox.value = false;
176-
toggle.value = false;
177-
input.value = '';
178-
inputOtp.value = '';
179-
range.value = {
180-
lower: 30,
181-
upper: 70
182-
};
183-
textarea.value = '';
184-
searchbar.value = '';
185-
datetime.value = '';
186-
radio.value = 'red';
187-
segment.value = 'dogs';
188-
select.value = 'apples';
129+
import { ref } from 'vue';
130+
131+
const checkbox = ref(false);
132+
const toggle = ref(false);
133+
const input = ref('');
134+
const inputOtp = ref('');
135+
const range = ref({
136+
lower: 30,
137+
upper: 70
138+
});
139+
const textarea = ref('');
140+
const searchbar = ref('');
141+
const datetime = ref('');
142+
const radio = ref('red');
143+
const segment = ref('dogs');
144+
const select = ref('apples');
145+
146+
const reset = () => {
147+
checkbox.value = false;
148+
toggle.value = false;
149+
input.value = '';
150+
inputOtp.value = '';
151+
range.value = {
152+
lower: 30,
153+
upper: 70
154+
};
155+
textarea.value = '';
156+
searchbar.value = '';
157+
datetime.value = '';
158+
radio.value = 'red';
159+
segment.value = 'dogs';
160+
select.value = 'apples';
161+
}
162+
163+
const set = () => {
164+
checkbox.value = true;
165+
toggle.value = true;
166+
input.value = 'Hello World';
167+
inputOtp.value = '1234';
168+
range.value = {
169+
lower: 10,
170+
upper: 90
171+
}
172+
textarea.value = 'Lorem Ipsum';
173+
searchbar.value = 'Search Query';
174+
datetime.value = '2019-01-31';
175+
radio.value = 'blue';
176+
segment.value = 'cats';
177+
select.value = 'bananas';
178+
}
179+
180+
const setIonicClasses = (element: HTMLElement, isBlurEvent: boolean) => {
181+
requestAnimationFrame(() => {
182+
let isValid = false;
183+
184+
// Handle ion-input-otp which has multiple inputs
185+
if (element.tagName === 'ION-INPUT-OTP') {
186+
const ionInputOtp = element as any;
187+
const value = ionInputOtp.value || '';
188+
const length = ionInputOtp.length || 4;
189+
// input-otp needs to check if all inputs are filled
190+
// (value length equals component length)
191+
isValid = value.length === length;
192+
// Handle ion-textarea which uses shadow DOM
193+
} else if (element.tagName === 'ION-TEXTAREA') {
194+
const nativeTextarea = element.shadowRoot?.querySelector('textarea') as HTMLTextAreaElement | null;
195+
if (nativeTextarea) {
196+
isValid = nativeTextarea.checkValidity();
197+
}
198+
// Handle ion-input which uses scoped encapsulation
199+
} else if (element.tagName === 'ION-INPUT') {
200+
const nativeInput = element.querySelector('input') as HTMLInputElement | null;
201+
if (nativeInput) {
202+
isValid = nativeInput.checkValidity();
203+
}
189204
}
190205
191-
const set = () => {
192-
checkbox.value = true;
193-
toggle.value = true;
194-
input.value = 'Hello World';
195-
inputOtp.value = '1234';
196-
range.value = {
197-
lower: 10,
198-
upper: 90
199-
}
200-
textarea.value = 'Lorem Ipsum';
201-
searchbar.value = 'Search Query';
202-
datetime.value = '2019-01-31';
203-
radio.value = 'blue';
204-
segment.value = 'cats';
205-
select.value = 'bananas';
206+
// Remove validation classes
207+
element.classList.remove('ion-valid', 'ion-invalid', 'ion-untouched');
208+
209+
// Mark as touched only on blur
210+
if (isBlurEvent) {
211+
element.classList.add('ion-touched');
206212
}
207213
208-
return {
209-
checkbox,
210-
toggle,
211-
input,
212-
inputOtp,
213-
range,
214-
textarea,
215-
searchbar,
216-
datetime,
217-
radio,
218-
segment,
219-
select,
220-
221-
reset,
222-
set
214+
// Add validation classes based on validity state
215+
if (isValid) {
216+
element.classList.add('ion-valid');
217+
} else {
218+
element.classList.add('ion-invalid');
223219
}
224-
}
225-
});
220+
});
221+
};
222+
223+
const handleValidation = (event: CustomEvent) => {
224+
const element = event.target as HTMLElement;
225+
if (!element) return;
226+
227+
const isBlurEvent = event.type === 'ionBlur';
228+
setIonicClasses(element, isBlurEvent);
229+
};
226230
</script>

packages/vue/test/base/tests/e2e/specs/inputs.cy.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,40 @@ describe('Inputs', () => {
6363
cy.get('#searchbar-ref').should('have.text', 'Hello Searchbar');
6464
});
6565
it('typing into textarea should update ref', () => {
66-
cy.get('ion-textarea textarea').type('Hello Textarea', { scrollBehavior: false });
66+
cy.get('ion-textarea').shadow().find('textarea').type('Hello Textarea', { scrollBehavior: false });
6767

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

0 commit comments

Comments
 (0)