feat: add native mobile swipe (#14014)
* feat: add native mobile swipe - add new swipe for native apps method - add docs - update `scrollIntoView` to make it depended on the `swipe` - update UTs for `scrollIntoView` - update and fix formatter and template @TODO: add UT's * feat: add duration - add ut's - update docs * chore: remove not used const * chore: update docs * chore: fixes after feedback
This commit is contained in:
parent
a35db04fab
commit
ba09aebe8e
11 changed files with 754 additions and 305 deletions
|
|
@ -45,4 +45,5 @@ export * from './browser/waitUntil.js'
|
|||
/**
|
||||
* Add it to the Browser scope, but not to the docs by providing a different folder
|
||||
*/
|
||||
export * from './mobile/swipe.js'
|
||||
export * from './mobile/tap.js'
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ const log = logger('webdriverio')
|
|||
*
|
||||
* :::info
|
||||
*
|
||||
* Scrolling for Mobile Native Apps is done based on native mobile gestures. It is only supported for the following drivers:
|
||||
* - [appium-uiautomator2-driver](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-scrollgesture) for Android
|
||||
* - [appium-xcuitest-driver](https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/#mobile-scroll) for iOS
|
||||
* Scrolling for Mobile Native Apps is done based on the mobile `swipe` command.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
|
|
@ -37,18 +35,26 @@ const log = logger('webdriverio')
|
|||
// scroll to a specific element in the default scrollable element for Android or iOS for a maximum of 10 scrolls
|
||||
await elem.scrollIntoView();
|
||||
// Scroll to the left in the scrollable element called '#scrollable' for a maximum of 5 scrolls
|
||||
await elem.scrollIntoView({ direction: 'left', maxScrolls: 5, scrollableElement: $('#scrollable') });
|
||||
await elem.scrollIntoView({
|
||||
direction: 'left',
|
||||
maxScrolls: 5,
|
||||
scrollableElement: $('#scrollable')
|
||||
});
|
||||
});
|
||||
* </example>
|
||||
*
|
||||
* @alias element.scrollIntoView
|
||||
* @param {object|boolean=} options options for `Element.scrollIntoView()`. Default for desktop/mobile web: <br/> `{ block: 'start', inline: 'nearest' }` <br /> Default for Mobile Native App <br /> `{ maxScrolls: 10, scrollDirection: 'down' }`
|
||||
* @rowInfo Desktop/Mobile Web Only
|
||||
* @param {string=} options.behavior See [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
|
||||
* @param {string=} options.block See [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
|
||||
* @param {string=} options.inline See [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
|
||||
* @param {string=} options.direction Can be one of `down`, `up`, `left` or `right`, default is `down`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @rowInfo Mobile Native App Only
|
||||
* @param {string=} options.direction Can be one of `down`, `up`, `left` or `right`, default is `up`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @param {number=} options.maxScrolls The max amount of scrolls until it will stop searching for the element, default is `10`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @param {number=} options.duration The duration in milliseconds for the swipe. Default is `1500` ms. The lower the value, the faster the swipe.<br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @param {Element=} options.scrollableElement Element that is used to scroll 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. <br /> <strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @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`.<br /><strong>NEVER</strong> 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.<br /> <strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @uses protocol/execute
|
||||
* @type utility
|
||||
*
|
||||
|
|
@ -63,7 +69,7 @@ export async function scrollIntoView (
|
|||
* Appium does not support the "wheel" action
|
||||
*/
|
||||
if (browser.isMobile) {
|
||||
if (await browser.getContext() === 'NATIVE_APP') {
|
||||
if (await browser.isNativeContext) {
|
||||
return nativeMobileScrollIntoView({
|
||||
browser,
|
||||
element: this,
|
||||
|
|
@ -139,51 +145,20 @@ type MobileScrollUntilVisibleOptions = {
|
|||
browser: WebdriverIO.Browser;
|
||||
element: WebdriverIO.Element;
|
||||
maxScrolls: number;
|
||||
scrollDirection: MobileScrollDirection;
|
||||
scrollableElement: WebdriverIO.Element | null;
|
||||
direction: MobileScrollDirection;
|
||||
scrollableElement?: WebdriverIO.Element | null;
|
||||
duration?: number;
|
||||
percent?: number;
|
||||
};
|
||||
|
||||
async function getScrollableElement({
|
||||
browser,
|
||||
options
|
||||
}: {
|
||||
browser: WebdriverIO.Browser,
|
||||
options?: MobileScrollIntoViewOptions
|
||||
}): Promise<WebdriverIO.Element | null> {
|
||||
if (options?.scrollableElement) {
|
||||
return options?.scrollableElement
|
||||
}
|
||||
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 elem.scrollIntoView({ scrollableElement: $('#scrollable') });
|
||||
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
async function mobileScrollUntilVisible({
|
||||
browser,
|
||||
direction,
|
||||
duration,
|
||||
element,
|
||||
scrollableElement,
|
||||
maxScrolls,
|
||||
scrollDirection,
|
||||
percent,
|
||||
scrollableElement,
|
||||
}: MobileScrollUntilVisibleOptions): Promise<{ hasScrolled: boolean; isVisible: boolean; }> {
|
||||
let isVisible = false
|
||||
let hasScrolled = false
|
||||
|
|
@ -198,20 +173,13 @@ async function mobileScrollUntilVisible({
|
|||
|
||||
if (isVisible) {break}
|
||||
|
||||
if (browser.isIOS) {
|
||||
await browser.execute('mobile: scroll', {
|
||||
elementId: (await scrollableElement)?.elementId,
|
||||
direction: scrollDirection,
|
||||
})
|
||||
hasScrolled = true
|
||||
} else {
|
||||
await browser.execute('mobile: scrollGesture', {
|
||||
elementId: (await scrollableElement)?.elementId,
|
||||
direction: scrollDirection,
|
||||
percent: 0.5,
|
||||
})
|
||||
hasScrolled = true
|
||||
}
|
||||
await browser.swipe({
|
||||
direction,
|
||||
...(duration ? { duration } : {}),
|
||||
...(percent ? { percent } : {}),
|
||||
...(scrollableElement ? { scrollableElement } : {}),
|
||||
})
|
||||
hasScrolled = true
|
||||
|
||||
scrolls++
|
||||
}
|
||||
|
|
@ -230,19 +198,20 @@ async function nativeMobileScrollIntoView({
|
|||
}) {
|
||||
const defaultOptions = {
|
||||
maxScrolls: 10,
|
||||
scrollDirection: MobileScrollDirection.Down,
|
||||
direction: MobileScrollDirection.Up,
|
||||
}
|
||||
const mobileOptions = {
|
||||
...defaultOptions,
|
||||
...(options || {}),
|
||||
}
|
||||
const scrollableElement = await getScrollableElement({ browser, options: mobileOptions })
|
||||
const { hasScrolled, isVisible } = await mobileScrollUntilVisible({
|
||||
browser,
|
||||
element,
|
||||
maxScrolls: mobileOptions.maxScrolls,
|
||||
scrollDirection: mobileOptions.scrollDirection,
|
||||
scrollableElement,
|
||||
direction: mobileOptions.direction,
|
||||
...(mobileOptions?.duration ? { duration: mobileOptions.duration } : {}),
|
||||
...(mobileOptions?.percent ? { percent: mobileOptions.percent } : {}),
|
||||
...(mobileOptions?.scrollableElement ? { scrollableElement: mobileOptions.scrollableElement } : {}),
|
||||
})
|
||||
|
||||
if (hasScrolled && isVisible) {
|
||||
|
|
@ -254,7 +223,7 @@ async function nativeMobileScrollIntoView({
|
|||
return
|
||||
}
|
||||
|
||||
throw new Error(`Element not found within scroll limit of ${mobileOptions.maxScrolls} scrolls by scrolling "${mobileOptions.scrollDirection}". ` +
|
||||
throw new Error(`Element not found within scroll limit of ${mobileOptions.maxScrolls} scrolls by scrolling "${mobileOptions.direction}". ` +
|
||||
`Are you sure the element is within the scrollable element or the direction is correct? You can change the scrollable element or direction like this:
|
||||
|
||||
await elem.scrollIntoView({
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@
|
|||
*
|
||||
* :::info
|
||||
*
|
||||
* Scrolling for Mobile Native Apps is done based on native mobile gestures. It is only supported for the following drivers:
|
||||
* - [appium-uiautomator2-driver](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-scrollgesture) for Android
|
||||
* - [appium-xcuitest-driver](https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/#mobile-scroll) for iOS
|
||||
* Scrolling for Mobile Native Apps is done based on the mobile `swipe` command.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
|
|
@ -28,21 +26,28 @@
|
|||
// scroll to a specific element in the default scrollable element for Android or iOS for a maximum of 10 scrolls
|
||||
await elem.scrollIntoView();
|
||||
// Scroll to the left in the scrollable element called '#scrollable' for a maximum of 5 scrolls
|
||||
await elem.scrollIntoView({ direction: 'left', maxScrolls: 5, scrollableElement: $('#scrollable') });
|
||||
await elem.scrollIntoView({
|
||||
direction: 'left',
|
||||
maxScrolls: 5,
|
||||
scrollableElement: $('#scrollable')
|
||||
});
|
||||
});
|
||||
* </example>
|
||||
*
|
||||
* @alias element.scrollIntoView
|
||||
* @param {object|boolean=} options options for `Element.scrollIntoView()`. Default for desktop/mobile web: <br/> `{ block: 'start', inline: 'nearest' }` <br /> Default for Mobile Native App <br /> `{ maxScrolls: 10, scrollDirection: 'down' }`
|
||||
* @rowInfo Desktop/Mobile Web Only
|
||||
* @param {string=} options.behavior See [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
|
||||
* @param {string=} options.block See [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
|
||||
* @param {string=} options.inline See [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView). <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
|
||||
* @param {string=} options.direction Can be one of `down`, `up`, `left` or `right`, default is `down`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @rowInfo Mobile Native App Only
|
||||
* @param {string=} options.direction Can be one of `down`, `up`, `left` or `right`, default is `up`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @param {number=} options.maxScrolls The max amount of scrolls until it will stop searching for the element, default is `10`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @param {number=} options.duration The duration in milliseconds for the swipe. Default is `1500` ms. The lower the value, the faster the swipe.<br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @param {Element=} options.scrollableElement Element that is used to scroll 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. <br /> <strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @mobileElement
|
||||
* @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`.<br /><strong>NEVER</strong> 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.<br /> <strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @uses protocol/execute
|
||||
* @type utility
|
||||
*
|
||||
* @skipUsage
|
||||
*/
|
||||
// actual implementation is located in packages/webdriverio/src/element/scrollIntoView.ts
|
||||
|
|
|
|||
228
packages/webdriverio/src/commands/mobile/swipe.ts
Normal file
228
packages/webdriverio/src/commands/mobile/swipe.ts
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import logger from '@wdio/logger'
|
||||
|
||||
import type { 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 <strong>AND</strong> 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.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* :::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.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* <example>
|
||||
: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();
|
||||
});
|
||||
* </example>
|
||||
*
|
||||
* <example>
|
||||
: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
|
||||
})
|
||||
});
|
||||
* </example>
|
||||
*
|
||||
* @alias element.scrollIntoView
|
||||
* @param {object|boolean=} options options for `Element.scrollIntoView()`. Default for desktop/mobile web: <br/> `{ block: 'start', inline: 'nearest' }` <br /> Default for Mobile Native App <br /> `{ maxScrolls: 10, scrollDirection: 'down' }`
|
||||
* @param {string=} options.direction Can be one of `down`, `up`, `left` or `right`, default is `up`. <br /><strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @rowInfo Down ::<strong>Starting Point:</strong><br/>You place your finger towards the top of the screen.<br/><strong>Movement:</strong><br/>You slide your finger downwards towards the bottom of the screen.<br/><strong>Action:</strong><br/>This also varies by context:<br />- On the home screen or in applications, it typically scrolls the content upwards.<br />- From the top edge, it often opens the notifications panel or quick settings.<br />- In browsers or reading apps, it can be used to scroll through content.
|
||||
* @rowInfo Left ::<strong>Starting Point:</strong><br/>You place your finger on the right side of the screen.<br/><strong>Movement:</strong><br/>You slide your finger horizontally to the left.><br/><strong>Action:</strong><br/>The response to this gesture depends on the application:<br />- It can move to the next item in a carousel or a set of images.<br />- In a navigation context, it might go back to the previous page or close the current view.<br />- On the home screen, it usually switches to the next virtual desktop or screen.
|
||||
* @rowInfo Right ::<strong>Starting Point:</strong><br/>You place your finger on the left side of the screen.<br/><strong>Movement:</strong><br/>You slide your finger horizontally to the right.<br/><strong>Action:</strong><br/>Similar to swiping left, but in the opposite direction:<br />-- It often moves to the previous item in a carousel or gallery.<br />- Can be used to open side menus or navigation drawers in apps.<br />- On the home screen, it typically switches to the previous virtual desktop.
|
||||
* @rowInfo Up ::<strong>Starting Point:</strong><br/>You place your finger towards the bottom of the screen.<br/><strong>Movement:</strong><br/>You slide your finger upwards towards the top of the screen.><br/><strong>Action:</strong><br/>Depending on the context, different actions can occur:<br />- On the home screen or in a list, this usually scrolls the content downwards.<br />- In a full-screen app, it might open additional options or the app drawer.<br />- 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. <br /> <strong>MOBILE-NATIVE-APP-ONLY</strong>
|
||||
* @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`.<br /><strong>NEVER</strong> 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.<br />This has no effect if `from` and `to` are provided.
|
||||
* @rowInfo The below values <strong>ONLY</strong> have an effect if the `scrollableElement` is <strong>NOT</strong> 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<void|unknown> {
|
||||
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
|
||||
}): 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(scrollableElement?.elementId)
|
||||
// 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 + height - height * swipePercentage) },
|
||||
right: { x: Math.round(x + width * swipePercentage), y: Math.round(y + height / 2) },
|
||||
bottom: { x: Math.round(x + width / 2), y: Math.round(y + height * swipePercentage) },
|
||||
left: { x: Math.round(x + width - width * swipePercentage), 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<WebdriverIO.Element> {
|
||||
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)
|
||||
}
|
||||
|
|
@ -531,10 +531,22 @@ export enum MobileScrollDirection {
|
|||
Right = 'right',
|
||||
}
|
||||
|
||||
export type MobileScrollIntoViewOptions = {
|
||||
export type XY = {
|
||||
x: number,
|
||||
y: number
|
||||
}
|
||||
|
||||
export type SwipeOptions = {
|
||||
direction?: MobileScrollDirection;
|
||||
duration?: number;
|
||||
from?: XY;
|
||||
percent?: number;
|
||||
scrollableElement?: WebdriverIO.Element ;
|
||||
to?: XY;
|
||||
}
|
||||
|
||||
export type MobileScrollIntoViewOptions = SwipeOptions & {
|
||||
maxScrolls?: number;
|
||||
scrollableElement?: WebdriverIO.Element;
|
||||
}
|
||||
|
||||
export interface CustomScrollIntoViewOptions extends ScrollIntoViewOptions, MobileScrollIntoViewOptions {
|
||||
|
|
|
|||
|
|
@ -100,37 +100,8 @@ exports[`scrollIntoView test > desktop > scrolls element when using boolean scro
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`scrollIntoView test > mobile Android app > does not find the default scroll element 1`] = `
|
||||
"Default scrollable element '//android.widget.ScrollView' was not found. Our advice is to provide a scrollable element like this:
|
||||
|
||||
await elem.scrollIntoView({ scrollableElement: $('#scrollable') });
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`scrollIntoView test > mobile Android app > scrolls by default the element to the top 1`] = `
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"direction": "down",
|
||||
"elementId": "scrollable-element-id",
|
||||
"percent": 0.5,
|
||||
},
|
||||
],
|
||||
"script": "mobile: scrollGesture",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`scrollIntoView test > mobile ios app > does not find the default scroll element 1`] = `
|
||||
"Default scrollable element '-ios predicate string:type == "XCUIElementTypeApplication"' was not found. Our advice is to provide a scrollable element like this:
|
||||
|
||||
await elem.scrollIntoView({ scrollableElement: $('#scrollable') });
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`scrollIntoView test > mobile ios app > does not find the element after max 5 scrolls 1`] = `
|
||||
"Element not found within scroll limit of 5 scrolls by scrolling "down". Are you sure the element is within the scrollable element or the direction is correct? You can change the scrollable element or direction like this:
|
||||
exports[`scrollIntoView test > mobile native app > does not find the element after max 5 scrolls 1`] = `
|
||||
"Element not found within scroll limit of 5 scrolls by scrolling "up". Are you sure the element is within the scrollable element or the direction is correct? You can change the scrollable element or direction like this:
|
||||
|
||||
await elem.scrollIntoView({
|
||||
direction: 'left' // possible options are: 'up|down|left|right'
|
||||
|
|
@ -139,29 +110,3 @@ await elem.scrollIntoView({
|
|||
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`scrollIntoView test > mobile ios app > scrolls by default the element to the top 1`] = `
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"direction": "down",
|
||||
"elementId": "scrollable-element-id",
|
||||
},
|
||||
],
|
||||
"script": "mobile: scroll",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`scrollIntoView test > mobile ios app > scrolls in a provided scroll-container to the left 1`] = `"{"using":"css selector","value":"#scrollContainer"}"`;
|
||||
|
||||
exports[`scrollIntoView test > mobile ios app > scrolls in a provided scroll-container to the left 2`] = `
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"direction": "left",
|
||||
"elementId": "some-elem-123",
|
||||
},
|
||||
],
|
||||
"script": "mobile: scroll",
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ describe('scrollIntoView test', () => {
|
|||
browserName: 'foobar',
|
||||
},
|
||||
})
|
||||
// @ts-expect-error
|
||||
elem = await browser.$('#foo')
|
||||
vi.spyOn(browser, 'getWindowSize').mockResolvedValue({ height: 800, width: 600 })
|
||||
vi.spyOn(browser, 'getElementRect').mockImplementation(() =>
|
||||
|
|
@ -90,6 +91,7 @@ describe('scrollIntoView test', () => {
|
|||
mobileMode: true
|
||||
} as any
|
||||
})
|
||||
// @ts-expect-error
|
||||
elem = await browser.$('#foo')
|
||||
// @ts-expect-error mock feature
|
||||
elem.elementId = { scrollIntoView: 'mockFunction' }
|
||||
|
|
@ -102,9 +104,8 @@ describe('scrollIntoView test', () => {
|
|||
it('scrolls by default the element to the top', async () => {
|
||||
await elem.scrollIntoView()
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
expect(calls).toHaveLength(2)
|
||||
const [[contextCallUrl], [executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(contextCallUrl.pathname).toEqual('/session/foobar-123/context')
|
||||
expect(calls).toHaveLength(1)
|
||||
const [[executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
|
||||
expect(JSON.parse(executeCallOptions.body).script).toEqual(
|
||||
expect.stringContaining('return ((elem, options2) => elem.scrollIntoView(options2)).apply(null, arguments)'))
|
||||
|
|
@ -115,9 +116,8 @@ describe('scrollIntoView test', () => {
|
|||
it('scrolls element when using boolean scroll options', async () => {
|
||||
await elem.scrollIntoView(true)
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
expect(calls).toHaveLength(2)
|
||||
const [[contextCallUrl], [executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(contextCallUrl.pathname).toEqual('/session/foobar-123/context')
|
||||
expect(calls).toHaveLength(1)
|
||||
const [[executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
|
||||
expect(JSON.parse(executeCallOptions.body).script).toEqual(
|
||||
expect.stringContaining('return ((elem, options2) => elem.scrollIntoView(options2)).apply(null, arguments)'))
|
||||
|
|
@ -128,9 +128,8 @@ describe('scrollIntoView test', () => {
|
|||
it('scrolls element using scroll into view options', async () => {
|
||||
await elem.scrollIntoView({ block: 'end', inline: 'center' })
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
expect(calls).toHaveLength(2)
|
||||
const [[contextCallUrl], [executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(contextCallUrl.pathname).toEqual('/session/foobar-123/context')
|
||||
expect(calls).toHaveLength(1)
|
||||
const [[executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
|
||||
expect(JSON.parse(executeCallOptions.body).script).toEqual(
|
||||
expect.stringContaining('return ((elem, options2) => elem.scrollIntoView(options2)).apply(null, arguments)'))
|
||||
|
|
@ -139,17 +138,19 @@ describe('scrollIntoView test', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('mobile ios app', () => {
|
||||
describe('mobile native app', () => {
|
||||
beforeAll(async () => {
|
||||
browser = await remote({
|
||||
capabilities: {
|
||||
mobileMode: true,
|
||||
nativeAppMode: true,
|
||||
platformName: 'iOS',
|
||||
platformVersion: '16',
|
||||
deviceName: 'iphone',
|
||||
app: 'foo.ipa',
|
||||
} as any
|
||||
})
|
||||
// @ts-expect-error
|
||||
elem = await browser.$('#foo')
|
||||
// @ts-expect-error mock feature
|
||||
elem.elementId = { scrollIntoView: 'mockFunction' }
|
||||
|
|
@ -157,7 +158,6 @@ describe('scrollIntoView test', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetch).mockClear()
|
||||
vi.spyOn(browser, 'getContext').mockResolvedValue('NATIVE_APP')
|
||||
vi.spyOn(browser, 'pause').mockResolvedValue(undefined)
|
||||
vi.spyOn(browser, '$$')
|
||||
})
|
||||
|
|
@ -172,11 +172,6 @@ describe('scrollIntoView test', () => {
|
|||
|
||||
await elem.scrollIntoView()
|
||||
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultIOSSelector)
|
||||
// call the mobileScrollUntilVisible but no scroll is needed
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
expect(calls).toHaveLength(0)
|
||||
|
|
@ -188,79 +183,42 @@ describe('scrollIntoView test', () => {
|
|||
elementId: 'scrollable-element-id',
|
||||
isDisplayed: vi.fn().mockResolvedValue(true),
|
||||
}])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
const isDisplayedSpy = vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
const swipeSpy = vi.spyOn(browser, 'swipe').mockResolvedValue(undefined)
|
||||
|
||||
await elem.scrollIntoView()
|
||||
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultIOSSelector)
|
||||
// call the mobileScrollUntilVisible
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
const [[executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
|
||||
expect(JSON.parse(executeCallOptions.body)).toMatchSnapshot()
|
||||
// browser.pause is called with the default value of 1000ms
|
||||
expect(browser.pause).toBeCalledTimes(1)
|
||||
expect(browser.pause).toBeCalledWith(1000)
|
||||
expect(isDisplayedSpy).toBeCalledTimes(2)
|
||||
expect(swipeSpy).toBeCalledTimes(1)
|
||||
expect(swipeSpy).toBeCalledWith({ direction: 'up' })
|
||||
|
||||
swipeSpy.mockRestore()
|
||||
isDisplayedSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('scrolls in a provided scroll-container to the left', async () => {
|
||||
it('scrolls to the left with some options', async () => {
|
||||
// Set some spy values
|
||||
(browser.$$ as Mock).mockResolvedValue([{
|
||||
elementId: 'scrollable-element-id',
|
||||
isDisplayed: vi.fn().mockResolvedValue(true),
|
||||
}])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
const isDisplayedSpy = vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
const swipeSpy = vi.spyOn(browser, 'swipe').mockResolvedValue(undefined)
|
||||
|
||||
await elem.scrollIntoView({
|
||||
scrollableElement: browser.$('#scrollContainer'),
|
||||
scrollDirection: 'left',
|
||||
// @ts-expect-error
|
||||
direction: 'left',
|
||||
duration: 3000,
|
||||
percent: 0.5,
|
||||
})
|
||||
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called but we have a provided scrollable element so it should not return the default iOS scrollable element
|
||||
expect((browser.$$ as Mock).mock.calls).toHaveLength(0)
|
||||
// call the mobileScrollUntilVisible
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
const [
|
||||
[scrollableElementCallUrl, scrollableElementCallOptions],
|
||||
[executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(calls).toHaveLength(2)
|
||||
// First check if the scrollable element is called
|
||||
expect(scrollableElementCallUrl.pathname).toEqual('/session/foobar-123/element')
|
||||
expect(scrollableElementCallOptions.body).toMatchSnapshot()
|
||||
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
|
||||
expect(JSON.parse(executeCallOptions.body)).toMatchSnapshot()
|
||||
// browser.pause is called with the default value of 1000ms
|
||||
expect(browser.pause).toBeCalledTimes(1)
|
||||
expect(browser.pause).toBeCalledWith(1000)
|
||||
})
|
||||
|
||||
it('does not find the default scroll element', async () => {
|
||||
// Set some spy values
|
||||
(browser.$$ as Mock).mockResolvedValue([])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValue(false)
|
||||
|
||||
try {
|
||||
await elem.scrollIntoView()
|
||||
} catch (err: any) {
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultIOSSelector)
|
||||
expect((browser.$$ as Mock).mock.calls).toHaveLength(1)
|
||||
// The mobileScrollUntilVisible is not called, meaning also the internal methods are also not called
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
console.log('err.message = ', err.message)
|
||||
expect(calls).toHaveLength(0)
|
||||
expect(err.message).toMatchSnapshot()
|
||||
}
|
||||
expect(isDisplayedSpy).toBeCalledTimes(2)
|
||||
expect(swipeSpy).toBeCalledTimes(1)
|
||||
expect(swipeSpy).toBeCalledWith({ direction: 'left', duration: 3000, percent: 0.5 })
|
||||
swipeSpy.mockRestore()
|
||||
isDisplayedSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('does not find the element after max 5 scrolls', async () => {
|
||||
|
|
@ -269,111 +227,15 @@ describe('scrollIntoView test', () => {
|
|||
elementId: 'scrollable-element-id',
|
||||
isDisplayed: vi.fn().mockResolvedValue(true),
|
||||
}])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValue(false)
|
||||
const isDisplayedSpy = vi.spyOn(elem, 'isDisplayed').mockResolvedValue(false)
|
||||
const swipeSpy = vi.spyOn(browser, 'swipe').mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
await elem.scrollIntoView({ maxScrolls: 5 })
|
||||
} catch (err: any) {
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultIOSSelector)
|
||||
// call the mobileScrollUntilVisible 5 times for the max scrolls
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
expect(calls).toHaveLength(5)
|
||||
expect(err.message).toMatchSnapshot()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('mobile Android app', () => {
|
||||
beforeAll(async () => {
|
||||
browser = await remote({
|
||||
capabilities: {
|
||||
mobileMode: true,
|
||||
platformName: 'Android',
|
||||
platformVersion: '14',
|
||||
deviceName: 'Samsung',
|
||||
app: 'foo.apk',
|
||||
} as any
|
||||
})
|
||||
elem = await browser.$('#foo')
|
||||
// @ts-expect-error mock feature
|
||||
elem.elementId = { scrollIntoView: 'mockFunction' }
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetch).mockClear()
|
||||
vi.spyOn(browser, 'getContext').mockResolvedValue('NATIVE_APP')
|
||||
vi.spyOn(browser, 'pause').mockResolvedValue(undefined)
|
||||
vi.spyOn(browser, '$$')
|
||||
})
|
||||
|
||||
it('no scroll is needed', async () => {
|
||||
// Set some spy values
|
||||
(browser.$$ as Mock).mockResolvedValue([{
|
||||
elementId: 'scrollable-element-id',
|
||||
isDisplayed: vi.fn().mockResolvedValue(true),
|
||||
}])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(true)
|
||||
|
||||
await elem.scrollIntoView()
|
||||
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultAndroidSelector)
|
||||
// call the mobileScrollUntilVisible but no scroll is needed
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('scrolls by default the element to the top', async () => {
|
||||
// Set some spy values
|
||||
(browser.$$ as Mock).mockResolvedValue([{
|
||||
elementId: 'scrollable-element-id',
|
||||
isDisplayed: vi.fn().mockResolvedValue(true),
|
||||
}])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
|
||||
await elem.scrollIntoView()
|
||||
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultAndroidSelector)
|
||||
// call the mobileScrollUntilVisible
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
const [[executeCallUrl, executeCallOptions]] = calls as any
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
|
||||
expect(JSON.parse(executeCallOptions.body)).toMatchSnapshot()
|
||||
// browser.pause is called with the default value of 1000ms
|
||||
expect(browser.pause).toBeCalledTimes(1)
|
||||
expect(browser.pause).toBeCalledWith(1000)
|
||||
})
|
||||
|
||||
it('does not find the default scroll element', async () => {
|
||||
// Set some spy values
|
||||
(browser.$$ as Mock).mockResolvedValue([])
|
||||
vi.spyOn(elem, 'isDisplayed').mockResolvedValue(false)
|
||||
|
||||
try {
|
||||
await elem.scrollIntoView()
|
||||
} catch (err: any) {
|
||||
// We're in the native context and will start scrolling
|
||||
expect(browser.getContext).toBeCalledTimes(1)
|
||||
// getScrollableElement is called and returns the default iOS scrollable element
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultAndroidSelector)
|
||||
expect((browser.$$ as Mock).mock.calls).toHaveLength(1)
|
||||
// The mobileScrollUntilVisible is not called, meaning also the internal methods are also not called
|
||||
const { calls } = vi.mocked(fetch).mock
|
||||
console.log('err.message = ', err.message)
|
||||
expect(calls).toHaveLength(0)
|
||||
expect(isDisplayedSpy).toBeCalledTimes(5)
|
||||
expect(swipeSpy).toBeCalledTimes(5)
|
||||
expect(err.message).toMatchSnapshot()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`swipe test > does not find the default scroll element for Android and throws an error 1`] = `
|
||||
[Error: Default scrollable element '//android.widget.ScrollView' was not found. Our advice is to provide a scrollable element like this:
|
||||
|
||||
await browser.swipe({ scrollableElement: $('#scrollable') });
|
||||
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`swipe test > should be able to swipe based on provided coordinates 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 100,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 10,
|
||||
"y": 20,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerDown",
|
||||
},
|
||||
{
|
||||
"duration": 10,
|
||||
"type": "pause",
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 1500,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 30,
|
||||
"y": 40,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerUp",
|
||||
},
|
||||
],
|
||||
"id": "action4",
|
||||
"parameters": {
|
||||
"pointerType": "touch",
|
||||
},
|
||||
"type": "pointer",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`swipe test > should call a swipe down command with 25% for a scrollable element 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 100,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 250,
|
||||
"y": 500,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerDown",
|
||||
},
|
||||
{
|
||||
"duration": 10,
|
||||
"type": "pause",
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 1500,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 250,
|
||||
"y": 300,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerUp",
|
||||
},
|
||||
],
|
||||
"id": "action8",
|
||||
"parameters": {
|
||||
"pointerType": "touch",
|
||||
},
|
||||
"type": "pointer",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`swipe test > should call a swipe down command with duration 5000 for a scrollable element 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 100,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 250,
|
||||
"y": 580,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerDown",
|
||||
},
|
||||
{
|
||||
"duration": 10,
|
||||
"type": "pause",
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 5000,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 250,
|
||||
"y": 220,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerUp",
|
||||
},
|
||||
],
|
||||
"id": "action12",
|
||||
"parameters": {
|
||||
"pointerType": "touch",
|
||||
},
|
||||
"type": "pointer",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`swipe test > should call a swipe left command with 75% for a scrollable element 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 100,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 325,
|
||||
"y": 400,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerDown",
|
||||
},
|
||||
{
|
||||
"duration": 10,
|
||||
"type": "pause",
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 1500,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 175,
|
||||
"y": 400,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerUp",
|
||||
},
|
||||
],
|
||||
"id": "action10",
|
||||
"parameters": {
|
||||
"pointerType": "touch",
|
||||
},
|
||||
"type": "pointer",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`swipe test > should call a swipe right command with 80% for a scrollable element 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 100,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 160,
|
||||
"y": 400,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerDown",
|
||||
},
|
||||
{
|
||||
"duration": 10,
|
||||
"type": "pause",
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 1500,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 340,
|
||||
"y": 400,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerUp",
|
||||
},
|
||||
],
|
||||
"id": "action11",
|
||||
"parameters": {
|
||||
"pointerType": "touch",
|
||||
},
|
||||
"type": "pointer",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`swipe test > should call a swipe up command with 45% for a scrollable element 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 100,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 250,
|
||||
"y": 380,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerDown",
|
||||
},
|
||||
{
|
||||
"duration": 10,
|
||||
"type": "pause",
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"duration": 1500,
|
||||
"origin": "viewport",
|
||||
"type": "pointerMove",
|
||||
"x": 250,
|
||||
"y": 420,
|
||||
},
|
||||
{
|
||||
"button": 0,
|
||||
"type": "pointerUp",
|
||||
},
|
||||
],
|
||||
"id": "action9",
|
||||
"parameters": {
|
||||
"pointerType": "touch",
|
||||
},
|
||||
"type": "pointer",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`swipe test > should throw an error a swipe command for direction foo 1`] = `[Error: Unknown direction: foo]`;
|
||||
|
||||
exports[`swipe test > should throw an error if the swipe command is called for a browser session 1`] = `[Error: The swipe command is only available for mobile platforms in the NATIVE context.]`;
|
||||
170
packages/webdriverio/tests/commands/mobile/swipe.test.ts
Normal file
170
packages/webdriverio/tests/commands/mobile/swipe.test.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import path from 'node:path'
|
||||
import type { Mock } from 'vitest'
|
||||
import { beforeEach, expect, describe, it, afterEach, vi, beforeAll } from 'vitest'
|
||||
import logger from '@wdio/logger'
|
||||
import { MobileScrollDirection, remote } from '../../../src/index.js'
|
||||
|
||||
vi.mock('fetch')
|
||||
const log = logger('test')
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
||||
let browser: WebdriverIO.Browser
|
||||
let elem: WebdriverIO.Element
|
||||
let rectSpy
|
||||
let logSpy
|
||||
const defaultAndroidSelector = '//android.widget.ScrollView'
|
||||
|
||||
describe('swipe test', () => {
|
||||
beforeEach(async () => {
|
||||
browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar',
|
||||
platformName: 'Android',
|
||||
app: 'app',
|
||||
mobileMode: true,
|
||||
nativeAppMode: true,
|
||||
} as any
|
||||
})
|
||||
// @ts-expect-error
|
||||
elem = await browser.$('#scrollableElement')
|
||||
rectSpy = vi.spyOn(browser, 'getElementRect').mockResolvedValue({ x: 100, y: 200, width: 300, height: 400 })
|
||||
logSpy = vi.spyOn(log, 'warn')
|
||||
vi.spyOn(browser, 'pause').mockResolvedValue(0)
|
||||
vi.spyOn(browser, '$$')
|
||||
})
|
||||
|
||||
it('should log a warning if swipe is called with a "scrollableElement" and "from"', async () => {
|
||||
await browser.swipe({
|
||||
scrollableElement: elem,
|
||||
from: { x: 10, y: 20 },
|
||||
})
|
||||
expect(logSpy).toHaveBeenCalledWith('`scrollableElement` is provided, so `from` and `to` will be ignored.')
|
||||
})
|
||||
|
||||
it('should log a warning if swipe is called with a "scrollableElement" and "to"', async () => {
|
||||
await browser.swipe({
|
||||
scrollableElement: elem,
|
||||
to: { x: 10, y: 20 },
|
||||
})
|
||||
expect(logSpy).toHaveBeenCalledWith('`scrollableElement` is provided, so `from` and `to` will be ignored.')
|
||||
})
|
||||
|
||||
it('should find the default scroll element for Android and can swipe', async () => {
|
||||
(browser.$$ as Mock).mockResolvedValue([{
|
||||
elementId: 'scrollable-android-element-id',
|
||||
}])
|
||||
|
||||
await browser.swipe()
|
||||
const [[selector]] = (browser.$$ as Mock).mock.calls
|
||||
expect(selector).toEqual(defaultAndroidSelector)
|
||||
})
|
||||
|
||||
it('does not find the default scroll element for Android and throws an error', async () => {
|
||||
(browser.$$ as Mock).mockResolvedValue([])
|
||||
|
||||
await expect(browser.swipe()).rejects.toThrowErrorMatchingSnapshot()
|
||||
})
|
||||
|
||||
it('should be able to swipe based on provided coordinates', async () => {
|
||||
await browser.swipe({
|
||||
from: { x: 10, y: 20 },
|
||||
to: { x: 30, y: 40 },
|
||||
})
|
||||
|
||||
expect(rectSpy).not.toBeCalled()
|
||||
// @ts-expect-error mock implementation
|
||||
expect(vi.mocked(fetch).mock.calls[2][0].pathname)
|
||||
.toBe('/session/foobar-123/actions')
|
||||
expect(JSON.parse(vi.mocked(fetch).mock.calls[2][1]?.body as any).actions[0])
|
||||
.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should log a warning if swipe percentage is not a number', async () => {
|
||||
await browser.swipe({
|
||||
direction: 'down',
|
||||
// @ts-expect-error invalid param
|
||||
percent: 'bar',
|
||||
scrollableElement: elem,
|
||||
})
|
||||
expect(logSpy).toHaveBeenCalledWith('The percentage to swipe should be a number.')
|
||||
})
|
||||
|
||||
it('should log a warning if swipe percentage is lower than 0', async () => {
|
||||
browser.swipe({
|
||||
direction: 'down',
|
||||
percent: -1,
|
||||
scrollableElement: elem,
|
||||
})
|
||||
expect(logSpy).toHaveBeenCalledWith('The percentage to swipe should be a number between 0 and 1.')
|
||||
})
|
||||
|
||||
it('should log a warning if swipe percentage is bigger than 1', async () => {
|
||||
await browser.swipe({
|
||||
direction: 'down',
|
||||
percent: 98,
|
||||
scrollableElement: elem,
|
||||
})
|
||||
expect(logSpy).toHaveBeenCalledWith('The percentage to swipe should be a number between 0 and 1.')
|
||||
})
|
||||
|
||||
const percentages = [0.25, 0.45, 0.75, 0.8]
|
||||
Object.values(MobileScrollDirection) .forEach((direction, index) => {
|
||||
const percent = percentages[index % percentages.length]
|
||||
|
||||
it(`should call a swipe ${direction} command with ${percent * 100}% for a scrollable element`, async () => {
|
||||
await browser.swipe({
|
||||
direction,
|
||||
scrollableElement: elem,
|
||||
percent,
|
||||
})
|
||||
expect(rectSpy).toHaveBeenCalledOnce()
|
||||
// @ts-expect-error mock implementation
|
||||
expect(vi.mocked(fetch).mock.calls[2][0].pathname)
|
||||
.toBe('/session/foobar-123/actions')
|
||||
expect(JSON.parse(vi.mocked(fetch).mock.calls[2][1]?.body as any).actions[0])
|
||||
.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call a swipe down command with duration 5000 for a scrollable element', async () => {
|
||||
await browser.swipe({
|
||||
duration: 5000,
|
||||
scrollableElement: elem,
|
||||
})
|
||||
|
||||
expect(rectSpy).toHaveBeenCalledOnce()
|
||||
// @ts-expect-error mock implementation
|
||||
expect(vi.mocked(fetch).mock.calls[2][0].pathname)
|
||||
.toBe('/session/foobar-123/actions')
|
||||
expect(JSON.parse(vi.mocked(fetch).mock.calls[2][1]?.body as any).actions[0])
|
||||
.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should throw an error a swipe command for direction foo', async () => {
|
||||
await expect(
|
||||
browser.swipe({
|
||||
direction: 'foo' as MobileScrollDirection,
|
||||
scrollableElement: elem
|
||||
})
|
||||
).rejects.toThrowErrorMatchingSnapshot()
|
||||
})
|
||||
|
||||
it('should throw an error if the swipe command is called for a browser session', async () => {
|
||||
browser = await remote({
|
||||
baseUrl: 'http://foobar.com',
|
||||
capabilities: {
|
||||
browserName: 'foobar',
|
||||
} as any
|
||||
})
|
||||
|
||||
await expect(browser.swipe()).rejects.toThrowErrorMatchingSnapshot()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(fetch).mockClear()
|
||||
rectSpy.mockRestore()
|
||||
logSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -63,7 +63,12 @@ await clock.<%= it.command %>(<%= it.paramString %>)
|
|||
<% it.paramTags.forEach((paramTag) => { %>
|
||||
<% if (paramTag.type === "rowInfo") { %>
|
||||
<tr>
|
||||
<td colspan="3"><strong><%= paramTag.string %></strong></td>
|
||||
<% if (paramTag.string.includes('::')) { %>
|
||||
<% const parts = paramTag.string.split('::').map(part => part.trim()); %>
|
||||
<td colspan="3"><strong><%= parts[0] %></strong><br /><%= parts[1] %></td>
|
||||
<% } else { %>
|
||||
<td colspan="3"><strong><%= paramTag.string %></strong></td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% } else { %>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ export default function (docfile: any) {
|
|||
paramStr.push(tag.name)
|
||||
} else if (tag.type === 'rowInfo') {
|
||||
paramTags.push(tag)
|
||||
paramStr.push(tag.string)
|
||||
} else if (tag.type === 'property') {
|
||||
tag.joinedTypes = Array.isArray(tag.types)
|
||||
? tag.types.join('|').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
|
|
|||
Loading…
Reference in a new issue