appium / python-client

Python language bindings for Appium
Apache License 2.0
1.65k stars 559 forks source link

Appium cannot find element by ID, but can by XPATH #509

Closed ws-cdavis closed 2 years ago

ws-cdavis commented 4 years ago

The problem

I've been working on dual Appium/Selenium test suite for a web page and mobile app. When using Appium, it is unable to find the element by ID. If I change it to use XPATH, it works as expected, then fails to find the next element by ID. This may not sound like a problem, but other elements in the app are not easily distinguished by XPATH values.

This test suite worked previously with manual app install and the Appium Service running on Mac. I've since tried to automate the process to install the application and run Appium Service programmatically. I am wondering if the way I've set this up is causing it not to detect elements by ID.

I know for certain the ID is correct. It works for the Selenium + web page combination, and I've tried digging into the app with Appium Studio. It displays the ID correctly in the app, and copy pasting the ID into my code doesn't change anything.

Environment

Link to Appium logs

Unfortunately, it doesn't look like the AppiumService in Python creates logs. If it does, I'll attach them when I find them or somebody tells me where they are

Code To Reproduce Issue [ Good To Have ]

Code below (redesign_tags and redesign_locators contain some sensitive info, but they work as intended in the Selenium flow):

class Driver:
    desired_capabilities = {}

    def __init__(self):
        # AppiumService was not called in previous version
        redesign_tags.appium_service = AppiumService()
        redesign_tags.appium_service.start()

        self.desired_capabilities[redesign_tags.platform_name_label] = redesign_tags.platform_device_name_android_label
        self.desired_capabilities[redesign_tags.no_reset_label] = redesign_tags.no_reset_value
        self.desired_capabilities[redesign_tags.platform_device_name_label] = redesign_tags.platform_device_name_galaxy_8
        self.desired_capabilities["app"] = "/Path/To/android-app.apk" # this line did not exist in the old version
        self.desired_capabilities[redesign_tags.app_package_label] = redesign_tags.app_package_value_dev
        self.desired_capabilities[redesign_tags.app_activity_label] = redesign_tags.app_activity_value
        self.instance = webdriver.Remote(redesign_tags.link_to_wd_hub, self.desired_capabilities)
        time.sleep(10)  # wait for application install
        print("Installed application")
def setupDevice():
    if redesign_tags.platform == "Android":
        print("Installing application...")
        driver = Driver()
    elif redesign_tags.platform == "Chrome":
        options = webdriver.ChromeOptions()
        options.add_argument('--start-maximized')
        options.add_argument('--unlimited-storage')
        driver = webdriver.Chrome(options=options)
        driver.get("localhost:8100")
        redesign_tags.webdriver = driver
        driver.set_window_position(0, 0)
        driver.maximize_window()
    else:
        print("Invalid platform specified")
        return False

    print("Registering device...")
    pairing = PairingPage(driver)
    # this element is not found by Appium
    pairing_credentials = pairing.get_element_by_id(redesign_locators.pairing_credentials_button, 25)
ki4070ma commented 4 years ago

@ws-aweaver Which info will you use for find_element_by_id ? (e.g. accessibility id from below image) Could you check your app with Appium Desktop?

SS_2020-02-29 17 27 14

ki4070ma commented 4 years ago

@ws-aweaver

You can see appium log on terminal by adding stdout=None as below. Could you take and attach appium log?

service = AppiumService()
service.start(stdout=None)
ws-cdavis commented 4 years ago

@ki4070ma Thank you so much for this! I've attached the full logs below, but I saw a few possible candidates for the issue:

[ADB] Using 'apkanalyzer' from '/Users/andrewweaver/Library/Android/sdk/tools/bin/apkanalyzer'
[debug] [ADB] Starting '/Users/andrewweaver/Library/Android/sdk/tools/bin/apkanalyzer' with args ["manifest","print","/Users/andrewweaver/.nvm/versions/node/v10.15.0/lib/node_modules/appium/node_modules/io.appium.settings/apks/settings_apk-debug.apk"]
[ADB] Cannot extract apk info using apkanalyzer. Falling back to aapt. Original error: Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
[ADB]   at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
[ADB]   at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
[ADB]   at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
[ADB]   at com.android.tools.apk.analyzer.ApkAnalyzerCli.getAaptInvokerFromSdk(ApkAnalyzerCli.java:277)
[ADB]   at com.android.tools.apk.analyzer.ApkAnalyzerCli.main(ApkAnalyzerCli.java:129)
[ADB] Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
[ADB]   at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
[ADB]   at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
[ADB]   at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
[ADB]   ... 5 more
[ADB] 
[ADB] Using 'aapt' from '/Users/andrewweaver/Library/Android/sdk/build-tools/29.0.2/aapt'

Maybe it is unable to analyze the apk to get all of the ids?

ADB] The application at 'PATH/TO/APP/android-app.apk' is already cached to '/data/local/tmp/appium_cache/06e635178a30e612dd2256a4d5401acf1b4eee8f.apk'
[debug] [ADB] Running '/Users/andrewweaver/Library/Android/sdk/platform-tools/adb -P 5037 -s 5200615242e31623 shell pm install /data/local/tmp/appium_cache/06e635178a30e612dd2256a4d5401acf1b4eee8f.apk'

It may be taking cached data from an old version of the application that did not have the IDs (doubtful, as I've verified the ID exists in Appium Studio)

Full logs:

Moved to https://gist.github.com/ki4070ma/7740f16c776c8b2e5540911c385c7559

ws-cdavis commented 4 years ago

@ws-aweaver Which info will you use for find_element_by_id ? (e.g. accessibility id from below image) Could you check your app with Appium Desktop?

SS_2020-02-29 17 27 14

I am using the ID value, not the accessibility id. I've verified the ID is correct using Appium Studio, which has similar functionality to the Appium Desktop you showed.

ki4070ma commented 4 years ago

@KazuCocoa @mykola-mokhnach

Is below result expected(as design)? It seems that android:id/text1 works, but io.appium.android.apis:id/text1 doesn't work.

Notes

    print(len(driver.find_elements_by_android_uiautomator('new UiSelector().resourceId("text1")')))
    print(len(driver.find_elements_by_android_uiautomator('new UiSelector().resourceId("id/text1")')))
    print(len(driver.find_elements_by_android_uiautomator('new UiSelector().packageName("android").resourceId("id/text1")')))
    print(len(driver.find_elements_by_android_uiautomator('new UiSelector().packageName("android").resourceId("text1")')))
    print(len(driver.find_elements_by_android_uiautomator('new UiSelector().packageName("io.appium.android.apis").resourceId("id/text1")')))
    print(len(driver.find_elements_by_android_uiautomator('new UiSelector().packageName("io.appium.android.apis").resourceId("text1")')))

Results

$python SampleAndroidTest_apidemo.py
0
12
0

Sample codes

#!/usr/bin/python
# -*- coding: utf-8 -*-

from appium import webdriver

caps = {
    'platformName': "Android",
    'deviceName': "Android Emulator",
    'appPackage': "io.appium.android.apis",
    'appActivity': ".ApiDemos",
    'automationName': "uiautomator2",
}

if __name__ == '__main__':
    driver = webdriver.Remote('http://localhost:4723/wd/hub', caps)

    # Found elements: 0
    els = driver.find_elements_by_id("id/text1")
    print(len(els))

    # Found elements: 12
    els = driver.find_elements_by_id("android:id/text1")
    print(len(els))

    # Found elements: 0
    els = driver.find_elements_by_id("io.appium.android.apis:id/text1")
    print(len(els))

    driver.quit()
KazuCocoa commented 4 years ago

Yes.

It depends on the app under test. You can find elements by io.appium.android.apis:id/text1 if the app under test had their customised resource id. Android's general component could have android: prefixed resource id if nothing developers specified.

The behaviour is not Appium specific.

ki4070ma commented 4 years ago

@ws-aweaver At first, you will use resource-id, right? If so, check if the id which you will use is same to the resource-id in appium desktop.

As https://github.com/appium/python-client/issues/509#issuecomment-596169368, it might be necessary to update used id.

ws-cdavis commented 4 years ago

I am trying to replicate this behavior in my code. I am using this function to wait for the element to appear:

def wait_for_element_by_id(self, id, time):
        try:
            WebDriverWait(self.driver, time).until(EC.visibility_of_element_located((MobileBy.ID, id)))
        except NoSuchElementException:
            print("Element not found: " + id)
            return False
        except TimeoutException:
            print("Timeout occurred: " + id)
            return False
        return True

My understanding is MobileBy.ID will not work. I do not see MobileBy.Resource_ID. I've also tried experimenting with just getting the element without a wait using this:

    def is_element_present_by_id(self, id):
        try:
            element = self.driver.find_element_by_id("android:id/"+id)
            if element is None:
                return False
        except NoSuchElementException:
            return False
        except TimeoutException:
            return False
        return True

I also tried element = self.driver.find_element_by_id("io.appium.android.apis:id/"+id) but this is not working either. The original code used self.driver.find_element(MobileBy.ID, id). I checked in Appium Studio. The ID and Resource ID are the same value.

I also tried downloading Appium Desktop just to be sure. It looks like the application I already have and was previously using to manually set up the Appium service. However, Mac is being difficult and says the app can't be opened with no explanation why. The version of Appium Desktop I already have doesn't have any element inspection services that I know of.

ws-cdavis commented 4 years ago

Update: I tried yesterday to demonstrate to somebody that XPATH works. It is no longer working. I also confirmed that I am in the correct context ("NATIVE_APP"). I am unsure what this means, as I have not changed the code in the application or the automation suite.

EDIT: XPATH is working correctly with UIAutomator2. It breaks on UIAutomator1. ID does not work with either. I also tried a cheap workaround by using XPATH to find an element with the ID //*[@resource-id='pairing-go-to-pair-by-login-button'] and this worked.

It looks like I need to find a method that looks by resource ID. I tried all of the methods below and they do not find the element: io.appium.android.apis:id/pairing-go-to-pair-by-login-button android:id/pairing-go-to-pair-by-login-button package_name:id/pairing-go-to-pair-by-login-button

I have the app set the ID programatically, but not the resource ID. The resource ID is the same as the regular ID, so for now I am able to progress with the ugly workaround. I would like to do this properly, though.

ws-cdavis commented 4 years ago

@ki4070ma How do I get resource ID properly in Appium Python Client? I didn't find a method for it. I used the ugly workaround in my above comment for now, and it looks like there is a serious performance issue. My test is taking around 15 minutes to execute. The old suite of 12 tests took around 20 minutes. I'd really like to get the execution time much lower.

Timwintle1979 commented 3 years ago

@ws-cdavis just wondering if you ever found a resolution for this? I'm afraid I'm not proficient in python like you clearly are so I'm utilising the appiumlibrary that's part of robot framework, however, I too have a problem that if I express as id=X it fails to find the element. If I express that same element as //*[@resource-id="X"] it works flawlessly.

Amulyakr commented 3 years ago

I am trying to replicate this behavior in my code. I am using this function to wait for the element to appear:

def wait_for_element_by_id(self, id, time):
        try:
            WebDriverWait(self.driver, time).until(EC.visibility_of_element_located((MobileBy.ID, id)))
        except NoSuchElementException:
            print("Element not found: " + id)
            return False
        except TimeoutException:
            print("Timeout occurred: " + id)
            return False
        return True

My understanding is MobileBy.ID will not work. I do not see MobileBy.Resource_ID. I've also tried experimenting with just getting the element without a wait using this:

    def is_element_present_by_id(self, id):
        try:
            element = self.driver.find_element_by_id("android:id/"+id)
            if element is None:
                return False
        except NoSuchElementException:
            return False
        except TimeoutException:
            return False
        return True

I also tried element = self.driver.find_element_by_id("io.appium.android.apis:id/"+id) but this is not working either. The original code used self.driver.find_element(MobileBy.ID, id). I checked in Appium Studio. The ID and Resource ID are the same value.

I also tried downloading Appium Desktop just to be sure. It looks like the application I already have and was previously using to manually set up the Appium service. However, Mac is being difficult and says the app can't be opened with no explanation why. The version of Appium Desktop I already have doesn't have any element inspection services that I know of.

Hey Did you find the way to find elements by id?

KazuCocoa commented 2 years ago

Basically id locator is for resource-id. https://github.com/appium/appium-uiautomator2-driver/blob/93be98ec9583ca498a2abad08e38f3220c0a5323/README.md#element-location (uia1 as well) UiAutomator's By.res is used as the backend.

ammaramja commented 2 years ago

Try specifying "chromeOptions": {"w3c": False} in your dc.

mzserroukh commented 2 years ago

I am facing a similar issue too. This does not work:by.ID("text") This works: By.XPATH("//*[@resource-id='text']")

Does anyone know why? potential fix ID directly?

KazuCocoa commented 2 years ago

It is via findObject by Android directly https://github.com/appium/appium-uiautomator2-server/blob/cef81e2a9755e103ff5a6fb488938a14ff8a7016/app/src/main/java/io/appium/uiautomator2/handler/FindElement.java#L82 or XPath info cached by Appium/UIA2 server from AccessibilityNodeInfo by Android (since Android does not support XPath natively).

The difference might cause it.

mykola-mokhnach commented 2 years ago

^ This could also be an issue with accessibility identifiers naming (app identifier prefix is missing). Then https://github.com/appium/appium/issues/15138#issuecomment-1127709588 could be used as a workaround.

HugoGresse commented 2 years ago

I'm having the same issue maybe:

I've attached two screenshots, the second one is to show that selecting with id: "org.plantnet:id/action_bar_root" does work. but the first one with id: "org.plantnet:id/bottomtabexplorer" or simply "bottomtabexplorer" does not work.

Here is my capabilities:

{
  "platformName": "Android",
  "appium:automationName": "UiAutomator2",
  "appium:appPackage": "org.plantnet",
  "appium:appActivity": "org.plantnet.MainActivity",
  "appium:disableIdLocatorAutocompletion": true
}
Capture d’écran 2022-06-27 à 16 28 05 Capture d’écran 2022-06-27 à 16 31 29
HugoGresse commented 2 years ago

it's crazy but the only thing that worked for me was to use

`xpath://*[@resource-id='${selector}']`

I had updated Appium globally, tried to change the capabilities to be more accurate and everything but this solve the issue right away. Before I even had strange behavior where I needed to click somewhere else for the next click to work.

mtaylor-evidation commented 2 years ago

Yes I cannot get the accessibility ID's to work either and like @HugoGresse I can only get it to find the elements by the xpath in appium with webdriver i/o latest versions. This is a react native app that I'm working with:

DC's are

capabilities: { platformName: "android", automationName: "Appium", platformVersion: "12", deviceName: "emulator-5554", app: "path-to-my-app", automationName: "UiAutomator2", autoGrantPermissions: 'true', }

I'm using an android nexus 6p with android 12

I have tried everything suggested.

This is blocking for me as one of the elements is off the screen and the scrolltoView is not implemented yet and I can't find elements that are not visible with xpaths. Any suggestions?

jlipps commented 2 years ago

@mtaylor-evidation if you're dealing with android you have a different problem--elements that are not on screen do not exist (android doesn't have any concept of an element being available/existing if it's not actually on screen). so you'll never be able to find it. the best practice for the uiautomator2 driver is to do what a user would do--perform a scroll and then check if the element is on screen, and repeat.

mtaylor-evidation commented 2 years ago

Totally agree,

Unfortunately, ScrollIntoView in webdriverio does appear to work. However, I know there may be other ways to scroll with appium, just trying to find the best way to do it. :)

jlipps commented 2 years ago

I don't know what "scroll into view" is. sounds like maybe it's a webdriverio thing. it's not an appium command. it's unclear to me how webdriverio implements it under the hood. it'd be better to do the correct appium thing, which is to use the actions api to construct a finger movement that will perform a scroll.

HugoGresse commented 2 years ago

I've made something like this to scroll the view down. It's not perfect but it works ~ok


/**
 * Scroll down by simulating a swipe down gesture.
 *
 * @param driver
 * @param scrollAmount does not work currently...
 * @param scrollDuration a lower scroll duration add more fling speed.
 */
export const scrollDown = async (driver, scrollAmount =1, scrollDuration = 300) => {
    const startPercentage = 90
    const endPercentage = 10
    const anchorPercentage = 50

    const { width, height } = await driver.getWindowSize()
    const density = (await driver.getDisplayDensity()) / 100
    const anchor = (width * anchorPercentage) / 100
    const startPoint = (height * startPercentage) / 100
    const endPoint = (height * endPercentage) / 100

    for (let i = 0; i < scrollAmount; i++) {
        await driver.performActions([
            {
                type: 'pointer',
                id: 'finger1',
                parameters: { pointerType: 'touch' },
                actions: [
                    { type: 'pointerMove', duration: 0, x: anchor, y: startPoint },
                    { type: 'pointerDown', button: 0 },
                    { type: 'pause', duration: 100 },
                    { type: 'pointerMove', duration: scrollDuration, origin: 'pointer', x: 0, y: -endPoint * density },
                    { type: 'pointerUp', button: 0 },
                    { type: 'pause', duration: scrollDuration },
                ],
            },
        ])
    }
}
mykola-mokhnach commented 2 years ago

@HugoGresse Check https://github.com/appium/appium/issues/15138

mtaylor-evidation commented 2 years ago

So amazed by this community. Although the solution proposed won't work for me (JavaScript is what I'm using) but I've really gleaning off of all of your knowledge. I appreciate your responses today I'll be heads down trying to figure it out for JavaScript maybe even consider branching out and using native webdriver

Nidhi3420009 commented 1 year ago

I am also facing issue to find the locator, I have mentioned the code below

package Mobile_Testing;

import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By; import org.openqa.selenium.remote.DesiredCapabilities;

import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.remote.MobileCapabilityType;

public class First_TestCase {

public static void main(String[] args) throws MalformedURLException, InterruptedException {
    File app = new File("D:\\Technogiq Work", "standard-bank.apk");
    DesiredCapabilities C=new DesiredCapabilities();
    C.setCapability(MobileCapabilityType.AUTOMATION_NAME , "uiautomator2");
    C.setCapability(MobileCapabilityType.PLATFORM_NAME,"Android");
    C.setCapability(MobileCapabilityType.DEVICE_NAME,"Android");
    C.setCapability(MobileCapabilityType.PLATFORM_VERSION,"7.0");

    C.setCapability("appPackage", "com.bank.standard");
    C.setCapability("appActivity", "com.bank.standard.MainActivity");

    URL url=new URL("http://0.0.0.0:4723/wd/hub");

    AndroidDriver driver=new AndroidDriver(url,C);

    Thread.sleep(2000);
    driver.findElement(By.xpath("//*[@resource-id, '${com.bank.standard:id/urs_nm}']")).sendKeys("1234");

driver.quit();

}}

I have also tried By.xpath("//[@resource-id, 'com.bank.standard:id/urs_nm']" By.xpath("//android.widget.EditText[@resource-id, 'com.bank.standard:id/urs_nm']") By.xpath("//*[contains(@resource-id, 'com.bank.standard:id/urs_nm')]") By.xpath("//com.bank.standard[@id='com.bank.standard:id/urs_nm']") By.id("com.bank.standard:id/urs_nm")

Screenshot 2022-11-22 192303

Nidhi3420009 commented 1 year ago

Please help me

Nidhi3420009 commented 1 year ago

Error on Selenium

Screenshot 2022-11-22 192505

mzserroukh commented 1 year ago

try replacing your Xpath to look like this: "//*[@resource-id='com.bank.standard:id/urs_nm']"

Nidhi3420009 commented 1 year ago

Hi thanks for the reply

I have tried but not working.