这个坑其实很早就遇到过了,当时靠着查看源码解决了这个问题(唉,看源码现在是必备技能啊),最近看到 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 的大致流程为:
通过排查各部分的代码发现,错误是在 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 时使用的属性名称):
可获取的:
字符串类型:
布尔类型(如果无特殊说明, get_attribute 里面使用的属性名称和 uiautomatorviewer 里面的一致):
exists()
方法,详情请看 http://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#exists())获取不到,但会显示在 uiautomatorviewer 中的属性:
我已经提了获取 content-desc 的 issue:https://github.com/appium/appium/issues/5142 ,后面看官方解释了。
后面有时间的话我也会看看能否完整地 fix 这个问题,然后给个 pull request 吧。