|
| 1 | +import { expect } from '@playwright/test'; |
| 2 | +import { configs, dragElementBy, test } from '@utils/test/playwright'; |
| 3 | + |
| 4 | +/** |
| 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 | + * |
| 9 | + * When an item has at least one expandable option and the user swipes |
| 10 | + * beyond the threshold (or with sufficient velocity), the item slides |
| 11 | + * off-screen, fires ionSwipe, and returns to its closed position. |
| 12 | + */ |
| 13 | + |
| 14 | +// Full animation cycle duration (100ms expand + 250ms off-screen + 300ms delay + 250ms return) |
| 15 | +const FULL_ANIMATION_MS = 1100; |
| 16 | + |
| 17 | +configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, config }) => { |
| 18 | + test.describe(title('item-sliding: full swipe'), () => { |
| 19 | + test.beforeEach(async ({ page }) => { |
| 20 | + await page.goto(`/src/components/item-sliding/test/full-swipe`, config); |
| 21 | + }); |
| 22 | + |
| 23 | + test('should fire ionSwipe when expandable option is swiped fully (end side)', async ({ |
| 24 | + page, |
| 25 | + }) => { |
| 26 | + const ionSwipe = await page.spyOnEvent('ionSwipe'); |
| 27 | + const item = page.locator('#expandable-end'); |
| 28 | + |
| 29 | + await dragElementBy(item, page, -190); |
| 30 | + await page.waitForTimeout(FULL_ANIMATION_MS); |
| 31 | + |
| 32 | + expect(ionSwipe.length).toBeGreaterThan(0); |
| 33 | + }); |
| 34 | + |
| 35 | + test('should fire ionSwipe when expandable option is swiped fully (start side)', async ({ |
| 36 | + page, |
| 37 | + }) => { |
| 38 | + const ionSwipe = await page.spyOnEvent('ionSwipe'); |
| 39 | + const item = page.locator('#expandable-start'); |
| 40 | + |
| 41 | + await dragElementBy(item, page, 190); |
| 42 | + await page.waitForTimeout(FULL_ANIMATION_MS); |
| 43 | + |
| 44 | + expect(ionSwipe.length).toBeGreaterThan(0); |
| 45 | + }); |
| 46 | + |
| 47 | + test('should return to closed state after full swipe animation completes', async ({ page }) => { |
| 48 | + const item = page.locator('#expandable-end'); |
| 49 | + |
| 50 | + await dragElementBy(item, page, -190); |
| 51 | + await page.waitForTimeout(FULL_ANIMATION_MS); |
| 52 | + await page.waitForChanges(); |
| 53 | + |
| 54 | + const openAmount = await item.evaluate((el: HTMLIonItemSlidingElement) => |
| 55 | + el.getOpenAmount() |
| 56 | + ); |
| 57 | + expect(openAmount).toBe(0); |
| 58 | + }); |
| 59 | + |
| 60 | + test('should NOT trigger full swipe animation for non-expandable options', async ({ page }) => { |
| 61 | + const ionSwipe = await page.spyOnEvent('ionSwipe'); |
| 62 | + const item = page.locator('#non-expandable'); |
| 63 | + |
| 64 | + await dragElementBy(item, page, -180); |
| 65 | + await page.waitForTimeout(600); |
| 66 | + |
| 67 | + // Non-expandable item should never fire ionSwipe |
| 68 | + expect(ionSwipe.length).toBe(0); |
| 69 | + }); |
| 70 | + }); |
| 71 | +}); |
| 72 | + |
| 73 | +/** |
| 74 | + * Velocity-based trigger: a fast short swipe should trigger the full animation |
| 75 | + * even if the raw distance alone wouldn't exceed the threshold. |
| 76 | + * This behavior does not vary across modes. |
| 77 | + */ |
| 78 | +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { |
| 79 | + test.describe(title('item-sliding: full swipe velocity'), () => { |
| 80 | + test('should trigger full swipe animation with fast velocity', async ({ page }) => { |
| 81 | + await page.goto(`/src/components/item-sliding/test/full-swipe`, config); |
| 82 | + |
| 83 | + const ionSwipe = await page.spyOnEvent('ionSwipe'); |
| 84 | + const item = page.locator('#expandable-end'); |
| 85 | + const box = (await item.boundingBox())!; |
| 86 | + |
| 87 | + // Few steps = high velocity gesture |
| 88 | + const startX = box.x + box.width - 10; |
| 89 | + const startY = box.y + box.height / 2; |
| 90 | + const endX = box.x + 30; |
| 91 | + |
| 92 | + await page.mouse.move(startX, startY); |
| 93 | + await page.mouse.down(); |
| 94 | + await page.mouse.move(endX, startY, { steps: 3 }); |
| 95 | + await page.mouse.up(); |
| 96 | + await page.waitForTimeout(FULL_ANIMATION_MS); |
| 97 | + |
| 98 | + expect(ionSwipe.length).toBeGreaterThan(0); |
| 99 | + }); |
| 100 | + }); |
| 101 | +}); |
| 102 | + |
| 103 | +/** |
| 104 | + * RTL support: swipe direction is mirrored. In RTL, swiping right |
| 105 | + * reveals the "end" side options and should trigger the full animation. |
| 106 | + */ |
| 107 | +configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEach( |
| 108 | + ({ title, config }) => { |
| 109 | + test.describe(title('item-sliding: full swipe'), () => { |
| 110 | + test('should fire ionSwipe in the correct swipe direction', async ({ page }) => { |
| 111 | + await page.goto(`/src/components/item-sliding/test/full-swipe`, config); |
| 112 | + |
| 113 | + const ionSwipe = await page.spyOnEvent('ionSwipe'); |
| 114 | + const item = page.locator('#expandable-end'); |
| 115 | + |
| 116 | + // In RTL the "end" side is on the left, revealed by dragging right |
| 117 | + const dragByX = config.direction === 'rtl' ? 190 : -190; |
| 118 | + |
| 119 | + await dragElementBy(item, page, dragByX); |
| 120 | + await page.waitForTimeout(FULL_ANIMATION_MS); |
| 121 | + |
| 122 | + expect(ionSwipe.length).toBeGreaterThan(0); |
| 123 | + }); |
| 124 | + }); |
| 125 | + } |
| 126 | +); |
0 commit comments