Skip to content

Instantly share code, notes, and snippets.

@wswebcreation
Created November 3, 2024 09:55
Show Gist options
  • Save wswebcreation/911e1cbf99c48d42f7e3733b54a22f62 to your computer and use it in GitHub Desktop.
Save wswebcreation/911e1cbf99c48d42f7e3733b54a22f62 to your computer and use it in GitHub Desktop.
WDIO Native scrollIntoView

Native scrollIntoView usage

Adjust the config like this

import { scrollIntoView } from "./scrollIntoView";

export const config: WebdriverIO.Config = {
  //...
  before: function (capabilities, specs) {
    browser.overwriteCommand("scrollIntoView", scrollIntoView, true);
  },
  //...
};

And use it like this

import { driver } from "@wdio/globals";

describe("Android Settings", () => {
  if (driver.isAndroid) {
    it("should open the Android settings and scroll", async () => {
      const elem = await $(
        '//android.widget.TextView[@resource-id="android:id/title" and @text="System"]'
      );
      await elem.scrollIntoView();
      await elem.click();
      await driver.pause(5000);
    });
  }

  if (driver.isIOS) {
    it("should open the iOS settings and scroll", async () => {
      const privacySecurityElem = await $(
        '-ios predicate string:name == "Privacy & Security"'
      );
      await privacySecurityElem.waitForDisplayed();
      await privacySecurityElem.click();
      const elem = await $(
        '-ios predicate string:name == "Speech Recognition"'
      );
      await elem.scrollIntoView();
      await elem.click();
      await driver.pause(5000);
    });
  }
});
/**
* 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);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment