想法很简单:将界面的ListView滚动到最底部。
由于看了webdriver的api貌似没有这个方法,如果有的话,麻烦哪位大神告诉下。所以就采用uiautomator进行实现了
代码:
driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(3)')
运行我们看看appium for windows的显示内容
Could not parse UiSelector argument: Must only call the 'scrollIntoView' method OR methods on UiScrollable which return UiScrollable or UiObject objects
好吧,估计这个用法有问题,行那我就不滚到底部,我就试试向前滚动一点点行吧
代码
driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true)).scrollForward()')
这个应该不会有问题了吧,我们看下appium Server的log吧
Could not parse UiSelector argument: methods must return UiScrollable or UiObject instances
乱了,这个到底是怎么回事,好吧还是老老实实去看下源代码吧。找到UiScrollableParser.java
private void applyArgsToMethod(Method method, ArrayList<String> arguments) throws UiSelectorSyntaxException {
StringBuilder sb = new StringBuilder();
for (String arg : arguments) {
sb.append(arg + ", ");
}
Logger.debug("UiScrollable invoking method: " + method + " args: " + sb.toString());
if (method.getGenericReturnType() == UiScrollable.class && returnedUiObject) {
throw new UiSelectorSyntaxException("Cannot call UiScrollable method \"" + method.getName() + "\" on a UiObject instance");
}
if (method.getGenericParameterTypes().length == 0) {
try {
scrollable = (UiScrollable)method.invoke(scrollable);
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (ClassCastException e) {
throw new UiSelectorSyntaxException("methods must return UiScrollable or UiObject instances");
}
}
else {
ArrayList<Object> convertedArgs = new ArrayList<Object>();
Type[] parameterTypes = method.getGenericParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
convertedArgs.add(coerceArgToType(parameterTypes[i], arguments.get(i)));
}
String methodName = method.getName();
Logger.debug("Method name: " + methodName);
boolean scrollIntoView = methodName.contentEquals("scrollIntoView");
if (method.getGenericReturnType() == UiScrollable.class || scrollIntoView) {
if (convertedArgs.size() > 1) {
throw new UiSelectorSyntaxException("No UiScrollable method that returns type UiScrollable takes more than 1 argument");
}
try {
if (scrollIntoView) {
Logger.debug("Setting uiObject for scrollIntoView");
UiSelector arg = (UiSelector) convertedArgs.get(0);
returnedUiObject = true;
uiObject = new UiObject(arg);
// scrollIntoView must return the object if it's already in view.
// without the exists check, the parser will error because there's no scrollable.
if (uiObject.exists()) {
return;
}
Logger.debug("Invoking method: " + method + " with: " + uiObject);
method.invoke(scrollable, uiObject);
Logger.debug("Invoke complete.");
} else {
scrollable = (UiScrollable)method.invoke(scrollable, convertedArgs.get(0));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (InvocationTargetException e) {
// Ignoring UiObjectNotFoundException as this handled during actual find.
if (e.getCause() instanceof UiObjectNotFoundException) {
Logger.debug("Ignoring UiObjectNotFoundException when using reflection to invoke method.");
return;
}
Logger.error(e.getCause().toString()); // we're only interested in the cause. InvocationTarget wraps the underlying problem.
throw new UiSelectorSyntaxException("problem using reflection to call this method");
}
}
else if (method.getGenericReturnType() == UiObject.class) {
returnedUiObject = true;
if (convertedArgs.size() == 2) {
try {
uiObject = (UiObject)method.invoke(scrollable, convertedArgs.get(0), convertedArgs.get(1));
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
}
} else if (convertedArgs.size() == 3) {
try {
uiObject = (UiObject)method.invoke(scrollable, convertedArgs.get(0), convertedArgs.get(1), convertedArgs.get(2));
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
}
}
else {
throw new UiSelectorSyntaxException("UiScrollable methods which return a UiObject have 2-3 args");
}
}
else {
throw new UiSelectorSyntaxException("Must only call the 'scrollIntoView' method OR methods on UiScrollable which return UiScrollable or UiObject objects");
}
}
}
问题就出在这里了,看前面的两个截图,都有打印UiScrollable invoking method xxxx 说明两个都运行到了这个,那我们首先来分析下
driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(3)')
if (method.getGenericParameterTypes().length == 0) {
xxxx
}
这里getGenericParameterTypes 返回了type类型的参数数组, 我们的方法scrollToEnd是有一个int参数的,所以不满足走else
那么我们再来看看else里面做了什么判断
这里我就不黏贴代码了,else首先判断返回值是否为UiScrollable或者方法名中带有scrollIntoView 这里我们不满足,看另外一个else 这里判断返回值类似是否为UiObject 很明显我们的返回时boolean也不满足,好吧只能走到最后了
throw new UiSelectorSyntaxException("Must only call the 'scrollIntoView' method OR methods on UiScrollable which return UiScrollable or UiObject objects");
这个就是为什么scrollToEnd调用失败的原因了
再来 看看我们的scrollForward,因为它是不带参数的所以满足第一个if语句,那为什么也是有异常的呢
try {
scrollable = (UiScrollable)method.invoke(scrollable);
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new UiSelectorSyntaxException("problem using reflection to call this method");
} catch (ClassCastException e) {
throw new UiSelectorSyntaxException("methods must return UiScrollable or UiObject instances");
}
调用反射的方法强制类型转换成UiScrollable,可是我们的返回值是boolean所以就出现ClassCastException了,这也是为什么scrollward能够执行成功,但是又报异常的原因的原因了。
结论
UiScrollable中的方法如果带有参数返回值不是UIScrollable又不是UiObject且方法名也不是scrollIntoView,那么就会调用失败
而如果方法不带参数的话,那么能够调用成功,但是会有异常报出。
=========================================================================================================
分析很不错,确实找到问题根源了。
不过 appium 支持 uiautomator 的 api 本来意图是用来找元素的(所以才叫 find_element_by_android_uiautomator
)。如果返回值不是一个元素,当然会报异常。
大致看了一下, appium 对于 uiautomator 语句的检查十分严格,要求必须返回 UiScrollable 或 UiObject ,并且严格限定了参数的写法。我试过用类似 new UiSelector().checked(new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(3))
的方式绕过返回值类型的限制,但没想到它竟然连 checked 的内容都限定为只能是 true
或 false
(boolean 类型),而对于参数类型为 string 的限定了参数内容第一个和最后一个字符必须是双引号。我尝试用 new UiSelector().fromParent
绕过(object 类型的检查是最松的),但结果还是不行(提示 Could not parse UiSelector argument: UiSelector has no new UiScrollable method
)
看来 appium 只支持 uiautomator api 中查找元素的部分,而不是完全支持 uiautomator api 。不过这也符合 find_element_by_android_uiautomator
这个方法本身的定位。
=======================================================================================================
appium有办法能使listview滚到底部吗?
有,但需要自己封装。