想法很简单:将界面的 ListView 滚动到最底部。
由于看了 webdriver 的 api 貌似没有这个方法,如果有的话,麻烦哪位大神告诉下。所以就采用 uiautomator 进行实现了
代码:


driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(3)')

运行我们看看 appium for windows 的显示内容

看 log methodName : scrollToEnd arg:3 解析都没问题,可是看到 Returning result,咦怎么是

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 吧

看运行结果确实向前滚动了,但是怎么又有错,而且跟前面的 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,那么就会调用失败
而如果方法不带参数的话,那么能够调用成功,但是会有异常报出。


↙↙↙阅读原文可查看相关链接,并与作者交流