@@ -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,155 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac
148152 } ) ;
149153} ) ;
150154
155+ /**
156+ * Test for Ionic theme that has a different full swipe animation behavior.
157+ */
158+ configs ( { modes : [ 'ionic-md' ] , directions : [ 'ltr' , 'rtl' ] } ) . forEach ( ( { title, config } ) => {
159+ test . describe ( title ( 'item-sliding: full swipe' ) , ( ) => {
160+ test ( 'should fire ionSwipe when expandable option is swiped fully (end side)' , async ( { page } ) => {
161+ await page . setContent (
162+ `
163+ <ion-item-sliding>
164+ <ion-item>
165+ <ion-label>Expandable End (Swipe Left)</ion-label>
166+ </ion-item>
167+ <ion-item-options side="end">
168+ <ion-item-option expandable="true">Delete</ion-item-option>
169+ </ion-item-options>
170+ </ion-item-sliding>
171+ ` ,
172+ config
173+ ) ;
174+
175+ const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
176+ const item = page . locator ( 'ion-item-sliding' ) ;
177+ await expect ( item ) . toBeVisible ( ) ;
178+ await page . waitForChanges ( ) ;
179+ const box = ( await item . boundingBox ( ) ) ! ;
180+ const y = box . y + box . height / 2 ;
181+
182+ // 1) Peek open (distance-based; moderate steps is fine)
183+ const peek = config . direction === 'rtl' ? 120 : - 120 ;
184+ await dragElementBy ( item , page , peek ) ;
185+
186+ // 2) Fast flick in the same direction as “full swipe”
187+ const startX = config . direction === 'rtl' ? box . x + 40 : box . x + box . width - 40 ;
188+ const endX = config . direction === 'rtl' ? box . x + box . width - 40 : box . x + 40 ;
189+ await page . mouse . move ( startX , y ) ;
190+ await page . mouse . down ( ) ;
191+ await page . mouse . move ( endX , y , { steps : 2 } ) ; // try 1–3; lower = faster
192+ await page . mouse . up ( ) ;
193+ await ionSwipe . next ( ) ;
194+ expect ( ionSwipe ) . toHaveReceivedEventTimes ( 1 ) ;
195+ } ) ;
196+
197+ test ( 'should fire ionSwipe when expandable option is swiped fully (start side)' , async ( { page } ) => {
198+ await page . setContent (
199+ `
200+ <ion-item-sliding>
201+ <ion-item>
202+ <ion-label>Expandable Start (Swipe Right)</ion-label>
203+ </ion-item>
204+ <ion-item-options side="start">
205+ <ion-item-option expandable="true">Archive</ion-item-option>
206+ </ion-item-options>
207+ </ion-item-sliding>
208+ ` ,
209+ config
210+ ) ;
211+
212+ const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
213+ const item = page . locator ( 'ion-item-sliding' ) ;
214+ await expect ( item ) . toBeVisible ( ) ;
215+ await page . waitForChanges ( ) ;
216+ const box = ( await item . boundingBox ( ) ) ! ;
217+ const y = box . y + box . height / 2 ;
218+
219+ // 1) Peek open (distance-based; moderate steps is fine)
220+ const peek = config . direction === 'rtl' ? - 120 : 120 ;
221+ await dragElementBy ( item , page , peek ) ;
222+
223+ // 2) Fast flick in the same direction as “full swipe”
224+ const startX = config . direction === 'rtl' ? box . x + box . width - 40 : box . x + 40 ;
225+ const endX = config . direction === 'rtl' ? box . x + 40 : box . x + box . width - 40 ;
226+ await page . mouse . move ( startX , y ) ;
227+ await page . mouse . down ( ) ;
228+ await page . mouse . move ( endX , y , { steps : 2 } ) ;
229+ await page . mouse . up ( ) ;
230+ await ionSwipe . next ( ) ;
231+ expect ( ionSwipe ) . toHaveReceivedEventTimes ( 1 ) ;
232+ } ) ;
233+
234+ test ( 'should return to closed state after full swipe animation completes' , async ( { page } ) => {
235+ await page . setContent (
236+ `
237+ <ion-item-sliding>
238+ <ion-item>
239+ <ion-label>Expandable End (Swipe Left)</ion-label>
240+ </ion-item>
241+ <ion-item-options side="end">
242+ <ion-item-option expandable="true">Delete</ion-item-option>
243+ </ion-item-options>
244+ </ion-item-sliding>
245+ ` ,
246+ config
247+ ) ;
248+
249+ const item = page . locator ( 'ion-item-sliding' ) ;
250+ await expect ( item ) . toBeVisible ( ) ;
251+ await page . waitForChanges ( ) ;
252+ const box = ( await item . boundingBox ( ) ) ! ;
253+ const y = box . y + box . height / 2 ;
254+
255+ // 1) Peek open (distance-based; moderate steps is fine)
256+ const peek = config . direction === 'rtl' ? - 120 : 120 ;
257+ await dragElementBy ( item , page , peek ) ;
258+
259+ // 2) Fast flick in the same direction as “full swipe”
260+ const startX = config . direction === 'rtl' ? box . x + box . width - 40 : box . x + 40 ;
261+ const endX = config . direction === 'rtl' ? box . x + 40 : box . x + box . width - 40 ;
262+ await page . mouse . move ( startX , y ) ;
263+ await page . mouse . down ( ) ;
264+ await page . mouse . move ( endX , y , { steps : 2 } ) ;
265+ await page . mouse . up ( ) ;
266+ await page . waitForTimeout ( FULL_ANIMATION_MS ) ;
267+
268+ const openAmount = await item . evaluate ( ( el : HTMLIonItemSlidingElement ) => el . getOpenAmount ( ) ) ;
269+ expect ( openAmount ) . toBe ( 0 ) ;
270+ } ) ;
271+
272+ test ( 'should NOT trigger full swipe animation for non-expandable options' , async ( { page } ) => {
273+ await page . setContent (
274+ `
275+ <ion-item-sliding>
276+ <ion-item>
277+ <ion-label>Non-Expandable (Should Show Options)</ion-label>
278+ </ion-item>
279+ <ion-item-options side="end">
280+ <ion-item-option>Edit</ion-item-option>
281+ </ion-item-options>
282+ </ion-item-sliding>
283+ ` ,
284+ config
285+ ) ;
286+ const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
287+ const item = page . locator ( 'ion-item-sliding' ) ;
288+ await expect ( item ) . toBeVisible ( ) ;
289+ await page . waitForChanges ( ) ;
290+ const dragByX = config . direction === 'rtl' ? 180 : - 180 ;
291+
292+ await dragElementBy ( item , page , dragByX ) ;
293+ await page . waitForChanges ( ) ;
294+
295+ expect ( ionSwipe ) . toHaveReceivedEventTimes ( 0 ) ;
296+
297+ const openAmount = await item . evaluate ( ( el : HTMLIonItemSlidingElement ) => el . getOpenAmount ( ) ) ;
298+
299+ expect ( Math . abs ( openAmount ) ) . toBeGreaterThan ( 0 ) ;
300+ } ) ;
301+ } ) ;
302+ } ) ;
303+
151304/**
152305 * Velocity-based trigger: a fast short swipe should trigger the full animation
153306 * even if the raw distance alone wouldn't exceed the threshold.
@@ -172,6 +325,8 @@ configs({ modes: ['md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config
172325
173326 const ionSwipe = await page . spyOnEvent ( 'ionSwipe' ) ;
174327 const item = page . locator ( 'ion-item-sliding' ) ;
328+ await expect ( item ) . toBeVisible ( ) ;
329+ await page . waitForChanges ( ) ;
175330 const box = ( await item . boundingBox ( ) ) ! ;
176331
177332 // Few steps = high velocity gesture
0 commit comments