关于 appium 调用 uiautomator 中 UiScorllable 遇到的问题及分析

想法很简单:将界面的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,那么就会调用失败
而如果方法不带参数的话,那么能够调用成功,但是会有异常报出。


=========================================================================================================

分析很不错,确实找到问题根源了。

不过 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滚到底部吗?

有,但需要自己封装。

  1. 首先找到 scrollView 的大小
  2. 计算每次 swipe 的距离和速度
  3. 不断 swipe ,直到 scrollView 的最后一行不再变化(最好加几个 retry,在最后一行和滑动前一样时再滑几次,避免遇到 scrollView 刚好滑动前后最后一行内容刚好一样)

你可能感兴趣的:(Appium)