import logger from '@wdio/logger' import type { ChainablePromiseElement, SwipeOptions, XY } from '../../types.js' import { MobileScrollDirection } from '../../types.js' const log = logger('webdriverio') const SWIPE_DEFAULTS = { DIRECTION: MobileScrollDirection.Up, DURATION: 1500, PERCENT: 0.95, } /** * * Swipe in a specific direction within viewport or element for Desktop/Mobile Web AND Mobile Native Apps. * * :::info * * Swiping for Mobile Native Apps is based on the W3C-actions protocol, simulating a finger press and movement. * This is different from the [`mobile: scrollGesture`](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-scrollgesture) for Android * or [`mobile: scroll`](https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/#mobile-scroll) for iOS command which is based on the Appium Driver protocol and is * only available for mobile platforms in the NATIVE context. * * This command only works with the following up-to-date components: * - Appium server (version 2.0.0 or higher) * - `appium-uiautomator2-driver` (for Android) * - `appium-xcuitest-driver` (for iOS) * * Make sure your local or cloud-based Appium environment is regularly updated to avoid compatibility issues. * * ::: * * :::caution Swiping based on coordinates * * Avoid using `from` and `to` options unless absolutely necessary. These are device-specific and may not work consistently across devices. * Use the `scrollableElement` option for reliable swipes within an element. * * ::: * * :swipe.js it('should execute a default swipe', async () => { // Default will be a swipe from the bottom to the top, meaning it will swipe UP await browser.swipe(); }); * * * :swipe.with.options.js it('should execute a swipe with options', async () => { await browser.swipe({ direction: 'left', // Swipe from right to left duration: 5000, // Last for 5 seconds percent: 0.5, // Swipe 50% of the scrollableElement scrollableElement: $('~carousel'), // The element to swipe within }) }); * * * @alias element.scrollIntoView * @param {object|boolean=} options options for `browser.swipe()`. Default for desktop/mobile web:
`{ direction: 'up', duration: 1500, percent: 0.95, scrollableElement: WebdriverIO.Element }` * @param {string=} options.direction Can be one of `down`, `up`, `left` or `right`, default is `up`.
MOBILE-NATIVE-APP-ONLY * @rowInfo Down ::Starting Point:
You place your finger towards the top of the screen.
Movement:
You slide your finger downwards towards the bottom of the screen.
Action:
This also varies by context:
- On the home screen or in applications, it typically scrolls the content upwards.
- From the top edge, it often opens the notifications panel or quick settings.
- In browsers or reading apps, it can be used to scroll through content. * @rowInfo Left ::Starting Point:
You place your finger on the right side of the screen.
Movement:
You slide your finger horizontally to the left.>
Action:
The response to this gesture depends on the application:
- It can move to the next item in a carousel or a set of images.
- In a navigation context, it might go back to the previous page or close the current view.
- On the home screen, it usually switches to the next virtual desktop or screen. * @rowInfo Right ::Starting Point:
You place your finger on the left side of the screen.
Movement:
You slide your finger horizontally to the right.
Action:
Similar to swiping left, but in the opposite direction:
-- It often moves to the previous item in a carousel or gallery.
- Can be used to open side menus or navigation drawers in apps.
- On the home screen, it typically switches to the previous virtual desktop. * @rowInfo Up ::Starting Point:
You place your finger towards the bottom of the screen.
Movement:
You slide your finger upwards towards the top of the screen.>
Action:
Depending on the context, different actions can occur:
- On the home screen or in a list, this usually scrolls the content downwards.
- In a full-screen app, it might open additional options or the app drawer.
- On certain interfaces, it could trigger a 'refresh' action or open a search bar. * @param {number=} options.duration The duration in milliseconds for the swipe. Default is `1500` ms. The lower the value, the faster the swipe. * @param {Element=} options.scrollableElement Element that is used to swipe within. If no element is provided it will use the following selector for iOS `-ios predicate string:type == "XCUIElementTypeApplication"` and the following for Android `//android.widget.ScrollView'`. If more elements match the default selector, then by default it will pick the first matching element.
MOBILE-NATIVE-APP-ONLY * @param {number=} options.percent The percentage of the (default) scrollable element to swipe. This is a value between 0 and 1. Default is `0.95`.
NEVER swipe from the exact top|bottom|left|right of the screen, you might trigger for example the notification bar or other OS/App features which can lead to unexpected results.
This has no effect if `from` and `to` are provided. * @rowInfo The below values ONLY have an effect if the `scrollableElement` is NOT provided, otherwise they are ignored. * @param {object=} options.from The x and y coordinates of the start of the swipe. If a `scrollableElement` is provided, then these coordinates have no effect. * @param {number=} options.from.x The x-coordinate of the start of the swipe. * @param {number=} options.from.y The y-coordinate of the start of the swipe. * @param {object=} options.to The x and y coordinates of the end of the swipe. If a `scrollableElement` is provided, then these coordinates have no effect. * @param {number=} options.to.x The x-coordinate of the end of the swipe. * @param {number=} options.to.y The y-coordinate of the end of the swipe. * @uses protocol/execute * @type utility * @skipUsage */ export async function swipe ( this: WebdriverIO.Browser, options?: SwipeOptions ): Promise { const browser = this if (!browser.isNativeContext) { throw new Error('The swipe command is only available for mobile platforms in the NATIVE context.') } let { scrollableElement, from, to } = options || {} if (scrollableElement && (from || to)) { log.warn('`scrollableElement` is provided, so `from` and `to` will be ignored.') } if (!from || !to) { scrollableElement = scrollableElement || await getScrollableElement(browser); ({ from, to } = await calculateFromTo({ browser, direction: options?.direction || SWIPE_DEFAULTS.DIRECTION, percentage: options?.percent, scrollableElement, })) } return w3cSwipe({ browser, duration: options?.duration || SWIPE_DEFAULTS.DURATION, from, to }) } async function calculateFromTo({ browser, direction, percentage, scrollableElement }: { browser: WebdriverIO.Browser, direction: `${MobileScrollDirection}`, percentage?: number, scrollableElement: WebdriverIO.Element | ChainablePromiseElement }): Promise<{ from: XY, to: XY }> { // 1. Determine the percentage of the scrollable container to be scrolled // The swipe percentage is the percentage of the scrollable container that should be scrolled // Never swipe from the exact top|bottom|left|right of the screen, you might trigger the notification bar or other OS/App features let swipePercentage = SWIPE_DEFAULTS.PERCENT if (percentage !== undefined) { if (isNaN(percentage)) { log.warn('The percentage to swipe should be a number.') } else if (percentage < 0 || percentage > 1) { log.warn('The percentage to swipe should be a number between 0 and 1.') } else { swipePercentage = percentage } } // 2. Determine the swipe coordinates // When we get the element rect we get the position of the element on the screen based on the // - x (position from the left of the screen) // - y (position from the top of the screen) // - width (width of the element) // - height (height of the element) // We can use this to calculate the position of the swipe by determining the // - top // - right // - bottom // - left // of the element. These positions will contain the x and y coordinates on where to put the finger const { x, y, width, height } = await browser.getElementRect(await scrollableElement?.elementId) // calculate the offset const verticalOffset = height - height * swipePercentage const horizontalOffset = width - width * swipePercentage // It's always advisable to swipe from the center of the element. const scrollRectangles = { top: { x: Math.round(x + width / 2), y: Math.round(y + verticalOffset / 2) }, right: { x: Math.round(x + width - horizontalOffset / 2), y: Math.round(y + height / 2) }, bottom: { x: Math.round(x + width / 2), y: Math.round(y + height - verticalOffset / 2 ) }, left: { x: Math.round(x + horizontalOffset / 2), y: Math.round(y + height / 2) }, } // 3. Swipe in the given direction let from: XY let to: XY switch (direction) { case MobileScrollDirection.Down: from = scrollRectangles.top to = scrollRectangles.bottom break case MobileScrollDirection.Left: from = scrollRectangles.right to= scrollRectangles.left break case MobileScrollDirection.Right: from = scrollRectangles.left to = scrollRectangles.right break case MobileScrollDirection.Up: from = scrollRectangles.bottom to = scrollRectangles.top break default: throw new Error(`Unknown direction: ${direction}`) } return { from, to } } async function getScrollableElement( browser: WebdriverIO.Browser): Promise { const defaultAndroidSelector = '//android.widget.ScrollView' const defaultIosSelector = '-ios predicate string:type == "XCUIElementTypeApplication"' const selector = browser.isIOS ? // For iOS, we need to find the application element, if we can't find it, we should throw an error defaultIosSelector : // There is always a scrollview for Android or, if this fails we should throw an error defaultAndroidSelector // Not sure why we need to do this, but it seems to be necessary const scrollableElements = (await browser.$$( selector )) as unknown as WebdriverIO.Element[] if (scrollableElements.length > 0) { return scrollableElements[0] } throw new Error( `Default scrollable element '${browser.isIOS ? defaultIosSelector : defaultAndroidSelector}' was not found. Our advice is to provide a scrollable element like this: await browser.swipe({ scrollableElement: $('#scrollable') }); ` ) } async function w3cSwipe({ browser, duration, from, to }: { browser: WebdriverIO.Browser, duration: number, from: XY, to: XY }) { await browser // a. Create the event .action('pointer', { parameters: { pointerType: browser.isMobile ? 'touch' : 'mouse' } }) // b. Move finger into start position .move(from.x, from.y) // This can also be written as .move({ x:from.x, y:from.y }) which allows you to add more options // c. Finger comes down into contact with screen .down() // d. Pause for a little bit .pause(10) // e. Finger moves to end position // IMPORTANT. The default duration, if you don't provide it, is 100ms. This means that the movement will be so fast that it: // - might not be registered // - might not have the correct result on longer movements. // Short durations will move elements on the screen over longer move coordinates very fast. .move({ duration: duration, x: to.x, y: to.y }) // f. Finger gets up, off the screen .up() // g. Perform the action .perform() // Add a pause, just to make sure the swipe is done // eslint-disable-next-line wdio/no-pause return browser.pause(500) }