diff --git a/core/src/components/item-sliding/test/basic/item-sliding.e2e.ts b/core/src/components/item-sliding/test/basic/item-sliding.e2e.ts index fa784af4090..de0ad68de65 100644 --- a/core/src/components/item-sliding/test/basic/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/basic/item-sliding.e2e.ts @@ -1,6 +1,12 @@ import { expect } from '@playwright/test'; import { configs, dragElementBy, test } from '@utils/test/playwright'; +import { + DRAG_DISTANCE_SINGLE_OPTION, + DRAG_DISTANCE_MULTIPLE_OPTIONS, + DRAG_STEPS_UNDER_FULL_SWIPE, +} from '../test.utils'; + /** * item-sliding doesn't have mode-specific styling, * but the child components, item-options and item-option, do. @@ -23,9 +29,9 @@ configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, conf * Positive dragByX value to drag element from the left to the right * to reveal the options on the left side. */ - const dragByX = config.direction === 'rtl' ? -100 : 100; + const dragByX = config.direction === 'rtl' ? -DRAG_DISTANCE_SINGLE_OPTION : DRAG_DISTANCE_SINGLE_OPTION; - await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, 20); + await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, DRAG_STEPS_UNDER_FULL_SWIPE); await page.waitForChanges(); await expect(item).toHaveScreenshot(screenshot('item-sliding-start')); @@ -42,9 +48,10 @@ configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, conf * Positive dragByX value to drag element from the left to the right * to reveal the options on the left side. */ - const dragByX = config.direction === 'rtl' ? 150 : -150; + const dragByX = config.direction === 'rtl' ? DRAG_DISTANCE_MULTIPLE_OPTIONS : -DRAG_DISTANCE_MULTIPLE_OPTIONS; - await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, 20); + await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, DRAG_STEPS_UNDER_FULL_SWIPE); + await page.waitForChanges(); await expect(item).toHaveScreenshot(screenshot('item-sliding-end')); }); @@ -61,7 +68,16 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co await page.goto(`/src/components/item-sliding/test/basic`, config); const item = page.locator('#item2'); - await dragElementBy(item, page, -150, 0, undefined, undefined, true, 20); + await dragElementBy( + item, + page, + -DRAG_DISTANCE_MULTIPLE_OPTIONS, + 0, + undefined, + undefined, + true, + DRAG_STEPS_UNDER_FULL_SWIPE + ); await page.waitForChanges(); // item-sliding doesn't have an easy way to tell whether it's fully open so just screenshot it @@ -140,8 +156,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, conf const direction = config.direction; const item = page.locator('ion-item-sliding'); - const dragByX = direction == 'rtl' ? -150 : 150; - await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, 20); + const dragByX = direction == 'rtl' ? -DRAG_DISTANCE_MULTIPLE_OPTIONS : DRAG_DISTANCE_MULTIPLE_OPTIONS; + await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, DRAG_STEPS_UNDER_FULL_SWIPE); await page.waitForChanges(); await expect(item).toHaveScreenshot(screenshot(`item-sliding-safe-area-left`)); @@ -181,8 +197,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, conf const direction = config.direction; const item = page.locator('ion-item-sliding'); - const dragByX = direction == 'rtl' ? 150 : -150; - await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, 20); + const dragByX = direction == 'rtl' ? DRAG_DISTANCE_MULTIPLE_OPTIONS : -DRAG_DISTANCE_MULTIPLE_OPTIONS; + await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, DRAG_STEPS_UNDER_FULL_SWIPE); await page.waitForChanges(); await expect(item).toHaveScreenshot(screenshot(`item-sliding-safe-area-right`)); diff --git a/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts b/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts index 1ba692bf5a3..38d09885389 100644 --- a/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts @@ -27,7 +27,7 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac Delete - `, + `, config ); @@ -52,7 +52,7 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac Archive - `, + `, config ); @@ -77,7 +77,7 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac Delete - `, + `, config ); @@ -132,7 +132,7 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac Edit - `, + `, config ); @@ -166,7 +166,7 @@ configs({ modes: ['md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config Delete - `, + `, config ); diff --git a/core/src/components/item-sliding/test/icons/item-sliding.e2e.ts b/core/src/components/item-sliding/test/icons/item-sliding.e2e.ts index f7bc0dc7e9a..4716a3054c5 100644 --- a/core/src/components/item-sliding/test/icons/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/icons/item-sliding.e2e.ts @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; import { configs, test, dragElementBy } from '@utils/test/playwright'; +import { DRAG_DISTANCE_MULTIPLE_OPTIONS, DRAG_STEPS_UNDER_FULL_SWIPE } from '../test.utils'; + /** * item-sliding doesn't have mode-specific styling, * but the child components, item-options and item-option, do. @@ -10,12 +12,13 @@ import { configs, test, dragElementBy } from '@utils/test/playwright'; */ configs({ modes: ['ionic-md', 'ios', 'md'] }).forEach(({ title, screenshot, config }) => { test.describe(title('item-sliding: icons'), () => { - test('should not have visual regressions', async ({ page }) => { + test.beforeEach(async ({ page }) => { await page.goto(`/src/components/item-sliding/test/icons`, config); + }); - const itemIDs = ['iconsOnly', 'iconsStart', 'iconsEnd', 'iconsTop', 'iconsBottom']; - for (const itemID of itemIDs) { - const item = page.locator(`#${itemID}`); + ['iconsOnly', 'iconsStart', 'iconsEnd', 'iconsTop', 'iconsBottom'].forEach((position) => { + test(`${position} - should not have visual regressions`, async ({ page }) => { + const item = page.locator(`#${position}`); /** * Negative dragByX value to drag element from the right to the left @@ -23,15 +26,15 @@ configs({ modes: ['ionic-md', 'ios', 'md'] }).forEach(({ title, screenshot, conf * Positive dragByX value to drag element from the left to the right * to reveal the options on the left side. */ - const dragByX = config.direction === 'rtl' ? 150 : -150; + const dragByX = config.direction === 'rtl' ? DRAG_DISTANCE_MULTIPLE_OPTIONS : -DRAG_DISTANCE_MULTIPLE_OPTIONS; - await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, 20); + await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, DRAG_STEPS_UNDER_FULL_SWIPE); await page.waitForChanges(); // Convert camelCase ids to kebab-case for screenshot file names - const itemIDKebab = itemID.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - await expect(item).toHaveScreenshot(screenshot(`item-sliding-${itemIDKebab}`)); - } + const positionKebab = position.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + await expect(item).toHaveScreenshot(screenshot(`item-sliding-${positionKebab}`)); + }); }); }); }); diff --git a/core/src/components/item-sliding/test/scroll-target/item-sliding.e2e.ts b/core/src/components/item-sliding/test/scroll-target/item-sliding.e2e.ts index 53772e7f02a..9fc0b5e9446 100644 --- a/core/src/components/item-sliding/test/scroll-target/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/scroll-target/item-sliding.e2e.ts @@ -1,5 +1,8 @@ import { expect } from '@playwright/test'; import { configs, test, dragElementBy } from '@utils/test/playwright'; + +import { DRAG_DISTANCE_MULTIPLE_OPTIONS } from '../test.utils'; + /** * This behavior does not vary across modes/directions */ @@ -15,7 +18,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0); - await dragElementBy(itemSlidingEl, page, -150, 0, undefined, undefined, false); + /** + * No need to increase steps to prevent the full swipe threshold + * from being crossed because: + * - we are not testing swipe behavior here + * - increasing steps is only in Webkit since it accumulates velocity + * faster than other browsers, and this test is skipped in Webkit, so + * the default step count is safe to use + */ + await dragElementBy(itemSlidingEl, page, -DRAG_DISTANCE_MULTIPLE_OPTIONS, 0, undefined, undefined, false); /** * Do not use scrollToBottom() or other scrolling methods diff --git a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts index 4bc4cd6007c..624196217a0 100644 --- a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts @@ -1,33 +1,33 @@ import { expect } from '@playwright/test'; import { configs, test, dragElementBy } from '@utils/test/playwright'; +import { DRAG_DISTANCE_MULTIPLE_OPTIONS, DRAG_STEPS_UNDER_FULL_SWIPE } from '../test.utils'; + /** * The shapes on the `item-option` do not vary by direction * when they are not being dragged. */ configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { test.describe(title('item-sliding: shapes'), () => { - test('should not have visual regressions when not expanded', async ({ page, skip }) => { + test.beforeEach(async ({ page }) => { await page.goto(`/src/components/item-sliding/test/shapes`, config); + }); - // TODO(FW-7288): Remove skip once fix has been implemented - skip.browser('webkit', 'Flaky test in Safari'); - - const itemIDs = ['round', 'soft', 'rectangular']; - for (const itemID of itemIDs) { - const item = page.locator(`#${itemID}`); + ['round', 'soft', 'rectangular'].forEach((shape) => { + test(`${shape} - should not have visual regressions when not expanded`, async ({ page }) => { + const item = page.locator(`#${shape}`); /** * Negative dragByX value to drag element from the right to the left * to reveal the options on the right side. */ - const dragByX = -150; + const dragByX = -DRAG_DISTANCE_MULTIPLE_OPTIONS; - await dragElementBy(item, page, dragByX); + await dragElementBy(item, page, dragByX, 0, undefined, undefined, true, DRAG_STEPS_UNDER_FULL_SWIPE); await page.waitForChanges(); - await expect(item).toHaveScreenshot(screenshot(`item-sliding-${itemID}`)); - } + await expect(item).toHaveScreenshot(screenshot(`item-sliding-${shape}`)); + }); }); }); }); diff --git a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png index d5c999183a4..f0ecfd908fd 100644 Binary files a/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png and b/core/src/components/item-sliding/test/shapes/item-sliding.e2e.ts-snapshots/item-sliding-soft-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/item-sliding/test/states/item-sliding.e2e.ts b/core/src/components/item-sliding/test/states/item-sliding.e2e.ts index 01c40bf2110..f30660803db 100644 --- a/core/src/components/item-sliding/test/states/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/states/item-sliding.e2e.ts @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; import { configs, test, dragElementBy } from '@utils/test/playwright'; +import { DRAG_DISTANCE_MULTIPLE_OPTIONS } from '../test.utils'; + /** * This behavior does not vary across modes */ @@ -16,8 +18,13 @@ configs({ modes: ['ionic-md', 'md', 'ios'], directions: ['ltr'] }).forEach(({ ti * Negative dragByX value to drag element from the right to the left * to reveal the options on the right side. */ - const dragByX = -150; + const dragByX = -DRAG_DISTANCE_MULTIPLE_OPTIONS; + /** + * No need to increase steps to prevent the full swipe threshold from + * being crossed because the option is disabled, so the option will + * never expand fully regardless of drag speed. + */ await dragElementBy(item, page, dragByX); await page.waitForChanges(); diff --git a/core/src/components/item-sliding/test/test.utils.ts b/core/src/components/item-sliding/test/test.utils.ts index 7bda6511efe..b18803b4af0 100644 --- a/core/src/components/item-sliding/test/test.utils.ts +++ b/core/src/components/item-sliding/test/test.utils.ts @@ -1,6 +1,21 @@ import { expect } from '@playwright/test'; import type { E2EPage, ScreenshotFn } from '@utils/test/playwright'; +/** + * Drag distances that reveal options without crossing the full swipe + * threshold (`optsWidth` + `SWIPE_MARGIN`). A narrower options panel + * requires a shorter drag. + */ +export const DRAG_DISTANCE_SINGLE_OPTION = 100; +export const DRAG_DISTANCE_MULTIPLE_OPTIONS = 150; + +/** + * The number of drag steps used when revealing options. A higher step + * count slows the drag velocity, keeping it below the full swipe + * threshold in WebKit. See `dragElementBy` for more details. + */ +export const DRAG_STEPS_UNDER_FULL_SWIPE = 15; + /** * Warning: This function will fail when in RTL mode. * TODO(FW-3711): Remove the `directions` config when this issue preventing diff --git a/core/src/utils/test/playwright/drag-element.ts b/core/src/utils/test/playwright/drag-element.ts index 3ec6c9fe162..81f6f9a6c9e 100644 --- a/core/src/utils/test/playwright/drag-element.ts +++ b/core/src/utils/test/playwright/drag-element.ts @@ -10,6 +10,18 @@ import type { ElementHandle, Locator } from '@playwright/test'; import type { E2EPage } from './'; +/** + * Drags an element by the given number of pixels on the X and Y axes. + * + * @param el The element to drag. + * @param page The E2E Page object. + * @param dragByX The number of pixels to drag on the X axis. Negative values drag left, positive values drag right. + * @param dragByY The number of pixels to drag on the Y axis. Negative values drag up, positive values drag down. + * @param startXCoord The X coordinate to start the drag from. Defaults to the center of the element. + * @param startYCoord The Y coordinate to start the drag from. Defaults to the center of the element. + * @param releaseDrag Whether to release the drag at the end of the gesture. Defaults to `true`. + * @param steps The number of steps to divide the drag into. More steps reduce velocity; fewer steps increase it. Use this to control whether velocity-based thresholds (e.g. full-swipe) are triggered, particularly in Safari where gesture velocity is calculated relative to animation frames. Defaults to `10`. + */ export const dragElementBy = async ( el: Locator | ElementHandle, page: E2EPage, @@ -46,10 +58,11 @@ export const dragElementBy = async ( /** * Drags an element by the given amount of pixels on the Y axis. + * * @param el The element to drag. * @param page The E2E Page object. - * @param dragByY The amount of pixels to drag the element by. - * @param startYCoord The Y coordinate to start the drag gesture at. Defaults to the center of the element. + * @param dragByY The number of pixels to drag on the Y axis. + * @param startYCoord The Y coordinate to start the drag from. Defaults to the center of the element. */ export const dragElementByYAxis = async ( el: Locator | ElementHandle, @@ -139,10 +152,21 @@ const moveElement = async (page: E2EPage, startX: number, startY: number, dragBy await page.mouse.move(middleX, middleY); - // Safari needs to wait for a repaint to occur before moving the mouse again. + /** + * In Safari, gesture velocity is calculated relative to animation + * frames, causing velocity to accumulate faster than in other + * browsers. Without waiting for a repaint, consecutive `mouse.move` + * events arrive with ~0ms time delta and velocity never accumulates, + * causing gesture + * detection to fail. + */ if (browser === 'webkit' && i % 2 === 0) { - // Repainting every 2 steps is enough to keep the drag gesture smooth. - // Anything past 4 steps will cause the drag gesture to be flaky. + /** + * Repainting every 2 steps is enough to keep the drag gesture + * smooth. Repainting on every step makes the test slow, and + * repainting every 4+ steps means Safari does not see enough + * frames to track the gesture reliably. + */ await page.evaluate(() => new Promise(requestAnimationFrame)); } }