@@ -2,10 +2,6 @@ import { expect } from '@playwright/test';
22import { configs , dragElementBy , test } from '@utils/test/playwright' ;
33
44/**
5- * Full swipe animation behavior is mode-independent but
6- * child components (ion-item-options, ion-item-option) have
7- * mode-specific styling, so we test across all modes.
8- *
95 * When an item has at least one expandable option and the user swipes
106 * beyond the threshold (or with sufficient velocity), the item slides
117 * off-screen, fires ionSwipe, and returns to its closed position.
@@ -14,7 +10,7 @@ import { configs, dragElementBy, test } from '@utils/test/playwright';
1410// Full animation cycle duration (100ms expand + 250ms off-screen + 300ms delay + 250ms return)
1511const FULL_ANIMATION_MS = 1100 ;
1612
17- configs ( { modes : [ 'ios' , 'md' , 'ionic-md' ] , directions : [ 'ltr' , 'rtl' ] } ) . forEach ( ( { title, config } ) => {
13+ configs ( { modes : [ 'ios' , 'md' ] , directions : [ 'ltr' , 'rtl' ] } ) . forEach ( ( { title, config } ) => {
1814 test . describe ( title ( 'item-sliding: full swipe' ) , ( ) => {
1915 test ( 'should fire ionSwipe when expandable option is swiped fully (end side)' , async ( { page } ) => {
2016 await page . setContent (
@@ -58,6 +54,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac
5854
5955 const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
6056 const item = page . locator ( 'ion-item-sliding' ) ;
57+ await expect ( item ) . toBeVisible ( ) ;
58+ await page . waitForChanges ( ) ;
6159 const dragByX = config . direction === 'rtl' ? - 190 : 190 ;
6260
6361 await dragElementBy ( item , page , dragByX ) ;
@@ -82,6 +80,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac
8280 ) ;
8381
8482 const item = page . locator ( 'ion-item-sliding' ) ;
83+ await expect ( item ) . toBeVisible ( ) ;
84+ await page . waitForChanges ( ) ;
8585 const dragByX = config . direction === 'rtl' ? 190 : - 190 ;
8686
8787 await dragElementBy ( item , page , dragByX ) ;
@@ -108,6 +108,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac
108108
109109 const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
110110 const item = page . locator ( 'ion-item-sliding' ) ;
111+ await expect ( item ) . toBeVisible ( ) ;
112+ await page . waitForChanges ( ) ;
111113 const dragByX = config . direction === 'rtl' ? 180 : - 180 ;
112114
113115 await dragElementBy ( item , page , dragByX ) ;
@@ -138,6 +140,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac
138140
139141 const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
140142 const item = page . locator ( 'ion-item-sliding' ) ;
143+ await expect ( item ) . toBeVisible ( ) ;
144+ await page . waitForChanges ( ) ;
141145 const dragByX = config . direction === 'rtl' ? 190 : - 190 ;
142146
143147 await dragElementBy ( item , page , dragByX ) ;
@@ -148,6 +152,157 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac
148152 } ) ;
149153} ) ;
150154
155+
156+ /**
157+ * Test for Ionic theme that has a different full swipe animation behavior.
158+ */
159+ configs ( { modes : [ 'ionic-md' ] , directions : [ 'ltr' , 'rtl' ] } ) . forEach ( ( { title, config } ) => {
160+ test . describe ( title ( 'item-sliding: full swipe' ) , ( ) => {
161+ test ( 'should fire ionSwipe when expandable option is swiped fully (end side)' , async ( { page } ) => {
162+ await page . setContent (
163+ `
164+ <ion-item-sliding>
165+ <ion-item>
166+ <ion-label>Expandable End (Swipe Left)</ion-label>
167+ </ion-item>
168+ <ion-item-options side="end">
169+ <ion-item-option expandable="true">Delete</ion-item-option>
170+ </ion-item-options>
171+ </ion-item-sliding>
172+ ` ,
173+ config
174+ ) ;
175+
176+ const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
177+ const item = page . locator ( 'ion-item-sliding' ) ;
178+ await expect ( item ) . toBeVisible ( ) ;
179+ await page . waitForChanges ( ) ;
180+ const box = ( await item . boundingBox ( ) ) ! ;
181+ const y = box . y + box . height / 2 ;
182+
183+ // 1) Peek open (distance-based; moderate steps is fine)
184+ const peek = config . direction === 'rtl' ? 120 : - 120 ;
185+ await dragElementBy ( item , page , peek ) ;
186+
187+ // 2) Fast flick in the same direction as “full swipe”
188+ const startX = config . direction === 'rtl' ? box . x + 40 : box . x + box . width - 40 ;
189+ const endX = config . direction === 'rtl' ? box . x + box . width - 40 : box . x + 40 ;
190+ await page . mouse . move ( startX , y ) ;
191+ await page . mouse . down ( ) ;
192+ await page . mouse . move ( endX , y , { steps : 2 } ) ; // try 1–3; lower = faster
193+ await page . mouse . up ( ) ;
194+ await ionSwipe . next ( ) ;
195+ expect ( ionSwipe ) . toHaveReceivedEventTimes ( 1 ) ;
196+ } ) ;
197+
198+ test ( 'should fire ionSwipe when expandable option is swiped fully (start side)' , async ( { page } ) => {
199+ await page . setContent (
200+ `
201+ <ion-item-sliding>
202+ <ion-item>
203+ <ion-label>Expandable Start (Swipe Right)</ion-label>
204+ </ion-item>
205+ <ion-item-options side="start">
206+ <ion-item-option expandable="true">Archive</ion-item-option>
207+ </ion-item-options>
208+ </ion-item-sliding>
209+ ` ,
210+ config
211+ ) ;
212+
213+ const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
214+ const item = page . locator ( 'ion-item-sliding' ) ;
215+ await expect ( item ) . toBeVisible ( ) ;
216+ await page . waitForChanges ( ) ;
217+ const box = ( await item . boundingBox ( ) ) ! ;
218+ const y = box . y + box . height / 2 ;
219+
220+ // 1) Peek open (distance-based; moderate steps is fine)
221+ const peek = config . direction === 'rtl' ? - 120 : 120 ;
222+ await dragElementBy ( item , page , peek ) ;
223+
224+ // 2) Fast flick in the same direction as “full swipe”
225+ const startX = config . direction === 'rtl' ? box . x + box . width - 40 : box . x + 40 ;
226+ const endX = config . direction === 'rtl' ? box . x + 40 : box . x + box . width - 40 ;
227+ await page . mouse . move ( startX , y ) ;
228+ await page . mouse . down ( ) ;
229+ await page . mouse . move ( endX , y , { steps : 2 } ) ;
230+ await page . mouse . up ( ) ;
231+ await ionSwipe . next ( ) ;
232+ expect ( ionSwipe ) . toHaveReceivedEventTimes ( 1 ) ;
233+ } ) ;
234+
235+
236+ test ( 'should return to closed state after full swipe animation completes' , async ( { page } ) => {
237+ await page . setContent (
238+ `
239+ <ion-item-sliding>
240+ <ion-item>
241+ <ion-label>Expandable End (Swipe Left)</ion-label>
242+ </ion-item>
243+ <ion-item-options side="end">
244+ <ion-item-option expandable="true">Delete</ion-item-option>
245+ </ion-item-options>
246+ </ion-item-sliding>
247+ ` ,
248+ config
249+ ) ;
250+
251+ const item = page . locator ( 'ion-item-sliding' ) ;
252+ await expect ( item ) . toBeVisible ( ) ;
253+ await page . waitForChanges ( ) ;
254+ const box = ( await item . boundingBox ( ) ) ! ;
255+ const y = box . y + box . height / 2 ;
256+
257+ // 1) Peek open (distance-based; moderate steps is fine)
258+ const peek = config . direction === 'rtl' ? - 120 : 120 ;
259+ await dragElementBy ( item , page , peek ) ;
260+
261+ // 2) Fast flick in the same direction as “full swipe”
262+ const startX = config . direction === 'rtl' ? box . x + box . width - 40 : box . x + 40 ;
263+ const endX = config . direction === 'rtl' ? box . x + 40 : box . x + box . width - 40 ;
264+ await page . mouse . move ( startX , y ) ;
265+ await page . mouse . down ( ) ;
266+ await page . mouse . move ( endX , y , { steps : 2 } ) ;
267+ await page . mouse . up ( ) ;
268+ await page . waitForTimeout ( FULL_ANIMATION_MS ) ;
269+
270+ const openAmount = await item . evaluate ( ( el : HTMLIonItemSlidingElement ) => el . getOpenAmount ( ) ) ;
271+ expect ( openAmount ) . toBe ( 0 ) ;
272+ } ) ;
273+
274+ test ( 'should NOT trigger full swipe animation for non-expandable options' , async ( { page } ) => {
275+ await page . setContent (
276+ `
277+ <ion-item-sliding>
278+ <ion-item>
279+ <ion-label>Non-Expandable (Should Show Options)</ion-label>
280+ </ion-item>
281+ <ion-item-options side="end">
282+ <ion-item-option>Edit</ion-item-option>
283+ </ion-item-options>
284+ </ion-item-sliding>
285+ ` ,
286+ config
287+ ) ;
288+ const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
289+ const item = page . locator ( 'ion-item-sliding' ) ;
290+ await expect ( item ) . toBeVisible ( ) ;
291+ await page . waitForChanges ( ) ;
292+ const dragByX = config . direction === 'rtl' ? 180 : - 180 ;
293+
294+ await dragElementBy ( item , page , dragByX ) ;
295+ await page . waitForChanges ( ) ;
296+
297+ expect ( ionSwipe ) . toHaveReceivedEventTimes ( 0 ) ;
298+
299+ const openAmount = await item . evaluate ( ( el : HTMLIonItemSlidingElement ) => el . getOpenAmount ( ) ) ;
300+
301+ expect ( Math . abs ( openAmount ) ) . toBeGreaterThan ( 0 ) ;
302+ } ) ;
303+ } ) ;
304+ } ) ;
305+
151306/**
152307 * Velocity-based trigger: a fast short swipe should trigger the full animation
153308 * even if the raw distance alone wouldn't exceed the threshold.
@@ -172,6 +327,8 @@ configs({ modes: ['md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config
172327
173328 const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
174329 const item = page . locator ( 'ion-item-sliding' ) ;
330+ const item = page . locator ( 'ion-item-sliding' ) ;
331+ await expect ( item ) . toBeVisible ( ) ;
175332 const box = ( await item . boundingBox ( ) ) ! ;
176333
177334 // Few steps = high velocity gesture
0 commit comments