Appium 关于 appium get_attribute 方法的坑

陈恒捷 · 2015年05月21日 · 最后由 时光清浅 回复于 2017年02月28日 · 4873 次阅读
本帖已被设为精华帖!

这个坑其实很早就遇到过了,当时靠着查看源码解决了这个问题(唉,看源码现在是必备技能啊),最近看到 Q 群上又有人踩坑了,所以花点时间写了一下如何躲坑

问题描述

当使用类似下面的代码获取元素的 content-desc 属性时,会报 NoSuchElement 错误:

# python
self.driver.find_element_by_id("id").get_attribute("content-desc")

但使用如下代码却能正常执行:

# python
self.driver.find_element_by_id("id").click()

很明显,这个错误原因不是找不到元素,而是 get_attribute 出问题。

问题原因探究

appium server 在 android 原生应用上获取 attribute 的大致流程为:

  1. 从 client 收到获取 attribute 的请求
  2. 把请求转发给在手机上运行的 bootstrap
  3. bootstrap 调用相关方法进行实际操作
  4. bootstrap 返回结果给 appium server
  5. appium server 把结果返回给 client

通过排查各部分的代码发现,错误是在 bootstrap 产生的(排查过程涉及代码有点多,所以这里就不解释了),所以看看 bootstrap 相关源码:

lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/GetAttribute.java

@Override
  public AndroidCommandResult execute(final AndroidCommand command)
      throws JSONException {
    if (command.isElementCommand()) {
      // only makes sense on an element
      final Hashtable<String, Object> params = command.params();

try {
// 获取需要操作的 element 实例
final AndroidElement el = command.getElement();
// 获取需要获取的 attribute 的名称
final String attr = params.get("attribute").toString();
if (attr.equals("name") || attr.equals("text")
|| attr.equals("className") || attr.equals("resourceId")) {
// 如果 attribute 名称为 name, text, className, resourceId ,调用元素的 getStringAttribute 方法获取
return getSuccessResult(el.getStringAttribute(attr));
} else {
// 否则调用 getBoolAttribute 获取(我们的 'content-desc' 执行的是这段语句)
return getSuccessResult(String.valueOf(el.getBoolAttribute(attr)));
}
} catch (final NoAttributeFoundException e) {
// 从这里开始就是第一个坑,无论是什么错误,最终返回的都是 NO_SUCH_ELEMENT
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // el is null
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
}
} else {
// 没有给出需要获取属性的 element ,返回错误信息
return getErrorResult("Unable to get attribute without an element.");
}
}


其中 `getStringAttribute` 和 `getBoolAttribute` 源码如下:

> lib/devices/android/bootstrap/src/io/appium/android/bootstrap/AndroidElement.java
```Java
public String getStringAttribute(final String attr)
      throws UiObjectNotFoundException, NoAttributeFoundException {
    String res;
    if (attr.equals("name")) {
      // 坑2:属性名称为 name 时,会先尝试获取 content-desc ,如果 content-desc 为空,则获取 text 。说白了,就算用 name 也不能保证你获取的就是 content-desc
      res = getContentDesc();
      if (res.equals("")) {
        res = getText();
      }
    } else if (attr.equals("text")) {
      res = getText();
    } else if (attr.equals("className")) {
      res = getClassName();
    } else if (attr.equals("resourceId")) {
      res = getResourceId();
    } else {
      throw new NoAttributeFoundException(attr);
    }
    return res;
  }
...
public boolean getBoolAttribute(final String attr)
      throws UiObjectNotFoundException, NoAttributeFoundException {
    boolean res;
    // 这个方法只会获取值为布尔类型的属性,我们的 content-desc 的值不是布尔类型的,所以抛出 NoAttributeFoundException
    if (attr.equals("enabled")) {
      res = el.isEnabled();
    } else if (attr.equals("checkable")) {
      res = el.isCheckable();
    } else if (attr.equals("checked")) {
      res = el.isChecked();
    } else if (attr.equals("clickable")) {
      res = el.isClickable();
    } else if (attr.equals("focusable")) {
      res = el.isFocusable();
    } else if (attr.equals("focused")) {
      res = el.isFocused();
    } else if (attr.equals("longClickable")) {
      res = el.isLongClickable();
    } else if (attr.equals("scrollable")) {
      res = el.isScrollable();
    } else if (attr.equals("selected")) {
      res = el.isSelected();
    } else if (attr.equals("displayed")) {
      res = el.exists();
    } else {
      throw new NoAttributeFoundException(attr);
    }
    return res;
  }

总结

1、获取 content-desc 的方法为 get_attribute("name") ,而且还不能保证返回的一定是 content-desc(content-desc 为空时会返回 text 属性值)
2、get_attribute 方法不是我们在 uiautomatorviewer 看到的所有属性都能获取的(此处的名称均为使用 get_attribute 时使用的属性名称):

可获取的:

字符串类型:

  • name(返回 content-desc 或 text)
  • text(返回 text)
  • className(返回 class,只有 API=>18 才能支持)
  • resourceId(返回 resource-id,只有 API=>18 才能支持)

布尔类型(如果无特殊说明, get_attribute 里面使用的属性名称和 uiautomatorviewer 里面的一致):

获取不到,但会显示在 uiautomatorviewer 中的属性:

  • index
  • package
  • password
  • bounds(可通过 get_position 来获取其中部分内容)

我已经提了获取 content-desc 的 issue:https://github.com/appium/appium/issues/5142 ,后面看官方解释了。
后面有时间的话我也会看看能否完整地 fix 这个问题,然后给个 pull request 吧。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 17 条回复 时间 点赞

大赞!!


这个坑我也踩过,当时我想做一个自动生成 xpath 的功能,优先级是,resourceId>text>name>index 我直接使用 xpath:://android.widget.Button[@index=2] 这种方式定位控件,后来发现 index 根本就用不起来,总是报没有该属性,我第一反应就是,index 不是 android 控件的属性,而 text、resourceId、content-desc 这些字段都可以在布局文件里面配置的,经过你这么系统的分析,恍然大悟啊,赞!

#2 楼 @cpfeng0124 xpath 其实和上面不一样, xpath 的工作原理是先 get source 出来,再在这个 xml 文件中找节点。
对于 xpath 的问题基本都能通过 get source 后在 source 中检查的方式解决。

#3 楼 @chenhengjie123 哦哦,不过,我好想 get source 出来后,没有 index 这个属性,而且用 xpath:://android.widget.Button[@index=2],通过 appium 去调用,好像找不到这个控件的哦。所以,后来我就去掉 index 方式了,吼吼

好长,辛苦了!

很赞的发现,期待官方的回复

#2 楼 @cpfeng0124 你这个 uiautomatorviewer 怎么有 xpath 选项,是跟安卓系统版本有关么。

#7 楼 @shijin880921 不是哦,是我二次开发加进去的哦

@cpfeng0124 求 uiautomatorviewer 开发过的源码~~

#8 楼 @cpfeng0124 这么好的东西应该分享出来呀

#10 楼 @sunrise 下周,保证分享,哎,时间时间啊

#11 楼 @cpfeng0124 期待你的分享,现在也遇到了一个 view 类型的 button,index 不起作用。index 不起作用,你是怎么实现去点击这个 button 的呢

最新消息,Appium 将在下个版本加入 get_attribute('contentDescription') 方法来获取 content-desc 属性:
https://github.com/appium/appium/pull/5189

太赞了,我也遇到了这个问题,虽然不是我解决的,但是可以找到解决问题的方法还是很开心。谢谢分享

这竟然是字符串, 不是布尔值。。

正准备放弃获取元素的 content-desc 属性时,有幸看到这篇文章,一下解了燃眉之急。真是一篇分析透彻、总结全面的好文章,受益匪浅!

huan [该话题已被删除] 中提及了此贴 06月27日 10:23
huan Appium Python API 中文版 By-HZJ 中提及了此贴 11月28日 10:30

多谢分享

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册