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

saii · 2015年08月08日 · 最后由 大东 回复于 2015年08月10日 · 2909 次阅读

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

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

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

不过 appium 支持 uiautomator 的 api 本来意图是用来找元素的(所以才叫 find_element_by_android_uiautomator )。如果返回值不是一个元素,当然会报异常。

大致看了一下, appium 对于 uiautomator 语句的检查十分严格,要求必须返回 UiScrollable 或 UiObject ,并且严格限定了参数的写法。我试过用类似 new UiSelector().checked(new UiScrollable(new UiSelector().scrollable(true)).scrollToEnd(3)) 的方式绕过返回值类型的限制,但没想到它竟然连 checked 的内容都限定为只能是 truefalse (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 这个方法本身的定位。

saii #2 · 2015年08月09日 Author

#1 楼 @chenhengjie123 那回到开始的问题,appium 有办法能使 listview 滚到底部吗?

#2 楼 @zsx10110 有,但需要自己封装。

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

对的。分析棒棒的。
所以我也是说,用的是 find_elements 方法,那你就要 return 一个 element 回来,那么理所当然什么 flingForward 之类的肯定是不行的。

需要 登录 後方可回應,如果你還沒有帳號按這裡 注册