|
/** |
|
* This file contains a custom implementation of the scrollIntoView function, it will |
|
* "override" the default scrollIntoView function for the WebdriverIO Element class. |
|
* The custom implementation is necessary for native mobile apps, as the default |
|
* scrollIntoView function does not work as expected. |
|
*/ |
|
export enum MobileScrollDirection { |
|
Down = "down", |
|
Up = "up", |
|
Left = "left", |
|
Right = "right", |
|
} |
|
|
|
interface CustomScrollIntoViewOptions extends ScrollIntoViewOptions { |
|
mobileOptions: { |
|
direction?: MobileScrollDirection; |
|
maxScrolls?: number; |
|
scrollableElement?: WebdriverIO.Element; |
|
}; |
|
} |
|
|
|
type MobileScrollUntilVisibleOptions = { |
|
element: WebdriverIO.Element; |
|
maxScrolls?: number; |
|
scrollDirection?: MobileScrollDirection; |
|
scrollableElement: WebdriverIO.Element | null; |
|
}; |
|
|
|
async function getScrollableElement( |
|
options?: CustomScrollIntoViewOptions |
|
): Promise<WebdriverIO.Element | null> { |
|
if (options?.mobileOptions?.scrollableElement) { |
|
return options.mobileOptions.scrollableElement; |
|
} |
|
|
|
const selector = driver.isAndroid |
|
? // There is always a scrollview for Android, if this fails we should throw an error |
|
"//android.widget.ScrollView" |
|
: // For iOS, we need to find the application element |
|
'-ios predicate string:type == "XCUIElementTypeApplication"'; |
|
// Not sure why we need to do this, but it seems to be necessary |
|
const scrollableElements = (await $$( |
|
selector |
|
)) as unknown as WebdriverIO.Element[]; |
|
if (scrollableElements.length > 0) { |
|
return scrollableElements[0]; |
|
} |
|
|
|
throw new Error( |
|
`Default scrollable element "//android.widget.ScrollView" not found.` |
|
); |
|
} |
|
|
|
async function mobileScrollUntilVisible({ |
|
element, |
|
scrollableElement, |
|
maxScrolls = 10, |
|
scrollDirection = MobileScrollDirection.Down, |
|
}: MobileScrollUntilVisibleOptions): Promise<boolean> { |
|
let isVisible = false; |
|
let scrolls = 0; |
|
|
|
while (!isVisible && scrolls < maxScrolls) { |
|
try { |
|
isVisible = await element.isDisplayed(); |
|
} catch { |
|
isVisible = false; |
|
} |
|
|
|
if (isVisible) break; |
|
|
|
if (browser.isAndroid) { |
|
await browser.execute("mobile: scrollGesture", { |
|
elementId: scrollableElement?.elementId, |
|
direction: scrollDirection, |
|
percent: 0.5, |
|
}); |
|
} else if (browser.isIOS) { |
|
await browser.execute("mobile: scroll", { |
|
elementId: scrollableElement?.elementId, |
|
direction: scrollDirection, |
|
}); |
|
} |
|
|
|
scrolls++; |
|
} |
|
|
|
return isVisible; |
|
} |
|
|
|
export const scrollIntoView = async function ( |
|
this: WebdriverIO.Element, |
|
origScrollIntoViewFunction: ( |
|
options?: boolean | CustomScrollIntoViewOptions |
|
) => Promise<void>, |
|
options?: boolean | CustomScrollIntoViewOptions |
|
) { |
|
const isNativeMobileApp = |
|
driver.isMobile && (await driver.getContext()) === "NATIVE_APP"; |
|
|
|
if (isNativeMobileApp) { |
|
// Handle custom scroll behavior for native mobile app |
|
const scrollableElement = await getScrollableElement( |
|
options as CustomScrollIntoViewOptions |
|
); |
|
|
|
const isVisible = await mobileScrollUntilVisible({ |
|
element: this, |
|
maxScrolls: (options as CustomScrollIntoViewOptions)?.mobileOptions |
|
?.maxScrolls, |
|
scrollDirection: (options as CustomScrollIntoViewOptions)?.mobileOptions |
|
?.direction, |
|
scrollableElement, |
|
}); |
|
|
|
if (isVisible) { |
|
// Pause for stabilization |
|
return driver.pause(1000); |
|
} else { |
|
throw new Error("Element not found within scroll limit"); |
|
} |
|
} else { |
|
// Use the original scrollIntoView function if not in the native mobile app context |
|
return origScrollIntoViewFunction.call(this, options); |
|
} |
|
}; |