107_一个跨平台的滑动 / 滚动助手的设计

转自appiumpro

测试的一个常见要求是滚动一个列表,直到找到一个元素或达到某个其他条件。 滚动可以通过多种方式实现,但是最“低级”的滚动方式是使用 Actions API。 在早期版本中,我们已经深入研究了这个 API,了解了如何自动化复杂的手势。 在那篇文章中,我们介绍了如何绘制形状! 但是一些更简单的东西呢,比如滚动列表?

这些原则都是一样的,但是对于一个行动来说是如此普遍,所以建立一些便利的方法来实现它是一个好主意。 这就是这篇文章的内容。 以下是我们的便利方法的要求:

  • 我们应该能够滚动列表视图
  • 我们应该能够以任意的方式滑动,在任意的时间内滑动
  • 我们应该能够定义一个滑动屏幕相关的术语,而不仅仅是绝对的像素
  • 我们的行为应该在 iOS 和 Android 上做同样的事情
  • 这些方法应该与合理的缺省值相等
    让我们深入研究一下如何做到这一点!

基类

因为我们希望在测试类之间共享这个功能,所以一定要将它放在某种基类上。 下面是我将要使用的基类(sans imports; 查看 GitHub 上的完整文件:

abstract public class Edition107_Base {
protected AppiumDriver<MobileElement> driver;

private By listView = MobileBy.AccessibilityId("List Demo");
private By firstCloud = MobileBy.AccessibilityId("Altocumulus");
private WebDriverWait wait;
private Dimension windowSize;

protected AppiumDriver<MobileElement> getDriver() {
return driver;
}

@After
public void tearDown() {
try {
getDriver().quit();
} catch (Exception ign) {}
}

protected void navToList() {
wait = new WebDriverWait(getDriver(), 10);
wait.until(ExpectedConditions.presenceOfElementLocated(listView)).click();
wait.until(ExpectedConditions.presenceOfElementLocated(firstCloud));
}
}

到目前为止,基类只负责导航到应用程序中的列表视图以及关闭应用程序。 注意,我们有一个名为 getDriver 的函数,它将被特定的测试用例覆盖,并允许每个测试用例返回自己的驱动程序(可能是 iOS 驱动程序或 Android 驱动程序)。 现在让我们开始构建便利的方法。

基础滑动

我们想做的任何事情(滚动、滑动)都可以从根本上被看作是一个基本上有4个步骤的动作:

  • 将手指移动到屏幕上方的一个位置
  • 用手指触摸屏幕
  • 当触摸屏幕时,将手指移动到另一个位置,这需要一定的时间
  • 把手指从屏幕上拿开
    对这些步骤进行编码将是我们的第一个任务,并将导致我们在其他任务的基础上构建方法。 让我们来看一下实现:
protected void swipe(Point start, Point end, Duration duration)

我们将此方法称为“ swipe” ,并允许用户定义起始点、结束点和持续时间。

AppiumDriver<MobileElement> d = getDriver();
boolean isAndroid = d instanceof AndroidDriver<?>;

为了执行滑动,我们需要得到的驱动程序实例,同时我们也给自己提供了一个方便的方法来判断该驱动程序是 iOS 还是 Android。

PointerInput input = new PointerInput(Kind.TOUCH, "finger1");
Sequence swipe = new Sequence(input, 0);
swipe.addAction(input.createPointerMove(Duration.ZERO, Origin.viewport(), start.x, start.y));
swipe.addAction(input.createPointerDown(MouseButton.LEFT.asArg()));

在上面的部分中,我们开始构建我们的动作序列。 首先,我们定义了指针输入和代表 swipe 的序列对象。 接下来的两个动作是 iOS 和 Android 共有的。 我们首先将指针移动到起始点(不需要显式的时间持续这样做) ,然后降低指针触摸屏幕。 现在,我们来看看一些代码,它们会根据平台的不同而有所变化:

if (isAndroid) {
duration = duration.dividedBy(ANDROID_SCROLL_DIVISOR);
} else {
swipe.addAction(new Pause(input, duration));
duration = Duration.ZERO;
}
swipe.addAction(input.createPointerMove(duration, Origin.viewport(), end.x, end.y));

这是函数的一部分,负责在一定时间内移动到终点。 这里有一个我们不得不担心的 iOS 和 Android 的怪癖。 对于 Android,我注意到动作所花费的实际时间总是远远大于我们实际指定的时间。 这可能不是全面的,但是我定义了一个静态变量 ANDROID scroll divisors 来表示这个值(我们在这里使用的值是3)。

在 iOS 上,指针移动的持续时间实际上根本没有在移动操作上编码,而是在移动操作之前插入的暂停操作上编码,这就是为什么我们在这里添加这个操作,如果我们使用 iOS 的话(在实际移动时将持续时间设置为零之前)。 这在 iOS 上有点奇怪,如果我们能找到一种方法来确保这些 api 在未来是等效的,我也不会感到惊讶。

swipe.addAction(input.createPointerUp(MouseButton.LEFT.asArg()));
d.perform(ImmutableList.of(swipe));

方法的最后一部分在上面,我们最终将手指从屏幕上抬起,并告诉驱动程序实际执行我们编码的序列。

相对地滑动

我们的 swipe 方法很棒,但是它需要绝对的屏幕坐标。 如果我们在不同屏幕尺寸的不同设备上运行相同的测试会怎么样? 如果我们能够根据屏幕的高度和宽度来指定我们的滑动,那会更好。 现在就开始吧。

private Dimension getWindowSize() {
if (windowSize == null) {
windowSize = getDriver().manage().window().getSize();
}
return windowSize;
}

protected void swipe(double startXPct, double startYPct, double endXPct, double endYPct, Duration duration) {
Dimension size = getWindowSize();
Point start = new Point((int)(size.width * startXPct), (int)(size.height * startYPct));
Point end = new Point((int)(size.width * endXPct), (int)(size.height * endYPct));
swipe(start, end, duration);
}

这里我们定义了滑动的新版本,它以百分比而不是绝对数字来表示起始值和结束值。 要做到这一点,我们需要另一个辅助函数来检索(和缓存)窗口大小。 使用窗口大小(高度和宽度) ,我们可以计算滑动的绝对坐标。

滚动

现在我们正处于一个很好的位置,建立我们的滚动相关的便利方法。 究竟什么是卷轴? 技术上来说,这是一种挥击,我们不关心其中一个维度。 如果我向下滚动一个列表,这意味着我在现实中正在执行一个缓慢向上滑动的动作,其中唯一的变化是在运动中的 y 轴的变化。

从概念上讲,我们有4个方向可以滚动,所以我们可以创建一个枚举来帮助我们定义:

public enum ScrollDirection {
UP, DOWN, LEFT, RIGHT
}

现在,我们可以构造一个取这些方向之一的 scroll 方法。 它还有助于使滚动的数量可配置。 我们想要一个短的卷轴还是一个长的卷轴? 我们所能做的就是允许用户根据屏幕的高度或宽度重新定义这个值。 所以如果我说我想要一个1的滚动量,这意味着我应该滚动相当于一个全屏幕。 0.5意味着相当于半个屏幕。 让我们来看看实现:

protected void scroll(ScrollDirection dir, double distance) {
if (distance < 0 || distance > 1) {
throw new Error("Scroll distance must be between 0 and 1");
}
Dimension size = getWindowSize();
Point midPoint = new Point((int)(size.width * 0.5), (int)(size.height * 0.5));
int top = midPoint.y - (int)((size.height * distance) * 0.5);
int bottom = midPoint.y + (int)((size.height * distance) * 0.5);
int left = midPoint.x - (int)((size.width * distance) * 0.5);
int right = midPoint.x + (int)((size.width * distance) * 0.5);
if (dir == ScrollDirection.UP) {
swipe(new Point(midPoint.x, top), new Point(midPoint.x, bottom), SCROLL_DUR);
} else if (dir == ScrollDirection.DOWN) {
swipe(new Point(midPoint.x, bottom), new Point(midPoint.x, top), SCROLL_DUR);
} else if (dir == ScrollDirection.LEFT) {
swipe(new Point(left, midPoint.y), new Point(right, midPoint.y), SCROLL_DUR);
} else {
swipe(new Point(right, midPoint.y), new Point(left, midPoint.y), SCROLL_DUR);
}
}

基本上,我们正在使用距离参数来定义我们正在构造的滑动的可能起点和终点应该在哪里。 从概念上讲,滚动滑动的起始点将是从屏幕中点到相应轴的距离的一半。 定义了所有这些点之后,我们就可以简单地从与我们想要滚动的方向相对应的适当点开始和结束了!

不错的默认滚动

在很多情况下,我们不需要确切地指定距离,所以我们可以依靠一个合理的默认值:

protected void scroll(ScrollDirection dir) {
scroll(dir, SCROLL_RATIO);
}

(在这个项目中,默认的距离是0.8,以确保我们不会意外地滚动超过我们可能关心的内容的像素)

一般来说,我们通常关心向下滚动列表,所以我们也可以有一个超级方便的方法来做到这一点:

protected void scroll() {
scroll(ScrollDirection.DOWN, SCROLL_RATIO);
}

就是这样! 这是一组帮助器方法,勾选上面所有的需求框。

把它们放在一起

让我们来看一个使用这些方法的跨平台示例,基本上是先向下滚动,然后在列表中向上滚动:

@Test
public void testGestures() {
navToList();
scroll();
scroll(ScrollDirection.UP);
}

这个方法既可以用于 iOS,也可以用于安卓测试,我为每个平台都编写了一个测试。