WWDC Quick Look 💓 By SwiftGGTeam
Explore Digital Crown, Trackpad, and iPad pointer automation

Explore Digital Crown, Trackpad, and iPad pointer automation

观看原视频

Highlight

Xcode 13 为 XCTest UI 测试加入 iPadOS 指针、watchOS Digital Crown 和 macOS 触控板滚动自动化 API,让开发者可以验证过去依赖真实输入设备的交互路径。

核心内容

iPad 应用支持鼠标和触控板之后,很多交互只在指针环境下出现。比如,用户把光标悬停在按钮上会看到动画,用两指横向滑动触控板会打开侧边栏。以前这类行为很难放进 UI 测试,测试只能覆盖触摸路径,指针路径要靠人工点一遍。

Xcode 13 把 iPadOS 指针变成 XCTest 可以驱动的输入源。测试代码可以执行悬停、点击、右键、双击、两指滚动、按住修饰键,以及点击后拖拽。测试目标从“页面能打开”推进到“使用 Magic Keyboard 时页面也按预期响应”。

这个能力有一个现实问题:同一个 App 可能同时跑在 iPad 和 iPhone 上。iPhone 不支持指针交互。如果直接运行指针测试,测试会失败。Xcode 13 同时提供 supportsPointerInteraction,测试可以在不支持的设备上跳过,避免把环境限制误报成产品缺陷。

watchOS 也有类似痛点。很多手表应用把 Digital Crown(数码表冠)当成核心输入方式,例如滚动时间、调节数值、浏览列表。Xcode 12.5 已经支持 watchOS UI 测试和表冠点击,Xcode 13 补上表冠旋转。测试可以指定旋转圈数和速度,验证界面文本是否跟着变化。

macOS 的问题在滚动。鼠标滚轮是离散滚动,触控板是连续滚动。过去 XCTest 在 macOS 上已有按像素滚动的方法,适合模拟鼠标滚轮。Xcode 13 新增类似触控板的连续 swipe 方法,用来测试带惯性、带速度的滚动行为。

详细内容

iPadOS 指针支持检测

01:28)Xcode 13 在 XCUIDevice 上加入 supportsPointerInteraction。它告诉测试当前设备是否支持指针交互。

extension XCUIDevice {

  public var supportsPointerInteraction: Bool

}

关键点:

  • XCUIDevice 表示当前 UI 测试运行的设备。
  • supportsPointerInteraction 返回布尔值。
  • iPad 指针测试可以先检查这个值,再决定运行或跳过。

04:36)完整测试里,Apple 用 XCTSkipUnless 跳过不支持指针交互的设备。

import XCTest

class Pointer_UI_Tests: XCTestCase {

  @available(iOS 15.0, *)
  func testHorizontalScrollRevealsSidebar() throws {
    try XCTSkipUnless(XCUIDevice.shared.supportsPointerInteraction,
                      "Device does not support pointer interaction")

    let app = XCUIApplication()
    app.launch()

    let sidebar = app.tables["Sidebar"]

    // Make sure sidebar menu is not present initially.
    XCTAssertFalse(sidebar.exists, "Sidebar should not be present initially")

    // Swipe horizontally to reveal sidebar.
    app.staticTexts["Select a smoothie"].scroll(byDeltaX: -200, deltaY: 0)

    // Verify sidebar is now present.
    XCTAssertTrue(sidebar.waitForExistence(timeout: 5),
                  "Sidebar did not appear within 5 second timeout")
  }

}

关键点:

  • @available(iOS 15.0, *) 标记这段测试只使用 iOS 15 之后可用的 API。
  • XCTSkipUnless 在条件不满足时跳过测试。
  • XCUIDevice.shared.supportsPointerInteraction 过滤掉不支持指针交互的设备。
  • XCUIApplication() 创建被测应用对象。
  • app.launch() 启动被测应用。
  • app.tables["Sidebar"] 找到无障碍标识或名称为 Sidebar 的表格。
  • XCTAssertFalse(sidebar.exists, ...) 验证侧边栏启动时不存在。
  • app.staticTexts["Select a smoothie"].scroll(byDeltaX: -200, deltaY: 0) 在指定文本元素上执行横向两指滚动。
  • waitForExistence(timeout: 5) 最多等待 5 秒,验证侧边栏出现。

iPadOS 指针交互方法

01:37)Xcode 13 在 XCUIElement 上加入一组指针方法。演讲还提到,这些方法也可以用在 XCUICoordinate,需要更精确坐标时可以使用坐标对象。

extension XCUIElement {
  open func hover()

  open func click()

  open func rightClick()

  open func doubleClick()

  open func scroll(byDeltaX: CGFloat, deltaY: CGFloat)

  open func click(forDuration: TimeInterval, thenDragToElement: XCUIElement)

  open func click(forDuration: TimeInterval, thenDragToElement: XCUIElement,
                  withVelocity: XCUIGestureVelocity, thenHoldForDuration: TimeInterval)

  open class func perform(withKeyModifiers flags: XCUIElement.KeyModifiers,
                          block: () -> Void)
}

关键点:

  • hover() 把指针移动到元素上,用来测试悬停动画或悬停状态。
  • click() 执行普通点击。
  • rightClick() 执行右键点击。
  • doubleClick() 执行双击。
  • scroll(byDeltaX:deltaY:) 执行两指滚动,deltaX 控制水平距离,deltaY 控制垂直距离。
  • click(forDuration:thenDragToElement:) 先按住,再拖到另一个元素。
  • withVelocitythenHoldForDuration 的重载可以指定拖拽速度和拖拽结束后的停留时间。
  • perform(withKeyModifiers:block:) 在一个代码块内按住修饰键执行操作。

watchOS Digital Crown 旋转

05:27)Xcode 13 在 XCUIDevice 上加入 rotateDigitalCrown。它接收旋转圈数,并可以指定速度。

// New rotateDigitalCrown API

extension XCUIDevice {

  open func rotateDigitalCrown(delta: CGFloat, velocity: XCUIGestureVelocity = .default)

}

关键点:

  • rotateDigitalCrown(delta:velocity:) 合成 Digital Crown 旋转事件。
  • delta 表示旋转圈数。
  • 正数表示向前旋转,负数表示向后旋转。
  • velocity 类型是 XCUIGestureVelocity
  • 速度可以使用 .slow.fast.default 预设,也可以使用自定义小数值,单位是每秒旋转圈数。

06:22)演讲用一个天气 App 说明测试方式。表冠向前滚动查看未来温度,向后滚动查看过去温度。

// Example use of watchOS Digital Crown API

func testForecastScrolling() {
  let app = XCUIApplication()
  app.launch()

  let forecastTime = app.staticTexts["forecast-time"]

  XCTAssertEqual(forecastTime.label, "Current Temperature")

  // Scroll 1 full rotation forward.
  XCUIDevice.shared.rotateDigitalCrown(delta: 1.0)
  XCTAssertEqual(forecastTime.label, "One hour from now")

  // Scroll 2 full rotations backward.
  XCUIDevice.shared.rotateDigitalCrown(delta: -2.0)
  XCTAssertEqual(forecastTime.label, "One hour ago")
}

关键点:

  • XCUIApplication() 创建 watchOS 被测应用对象。
  • app.launch() 启动应用。
  • app.staticTexts["forecast-time"] 找到显示预测时间的文本。
  • 第一个 XCTAssertEqual 验证初始状态是当前温度。
  • rotateDigitalCrown(delta: 1.0) 向前旋转一整圈。
  • 第二个 XCTAssertEqual 验证界面更新到一小时后。
  • rotateDigitalCrown(delta: -2.0) 向后旋转两整圈。
  • 最后一个 XCTAssertEqual 验证界面更新到一小时前。

macOS 离散滚动和连续滚动

07:46)macOS 上原有的 scroll(byDeltaX:deltaY:) 适合离散滚动。演讲把它类比为鼠标滚轮:每一格移动固定距离,停止输入后内容立刻停止。

extension XCUIElement {

  // Use the existing API for discrete (mouse wheel-like) scrolling.
  open func scroll(byDeltaX: CGFloat, deltaY: CGFloat)

}

关键点:

  • scroll(byDeltaX:deltaY:) 是已有 API。
  • deltaX 指定水平滚动距离。
  • deltaY 指定垂直滚动距离。
  • 这个方法用于像素级、非惯性的滚动测试。

08:05)Xcode 13 新增连续滚动方法,适合模拟触控板两指滑动。连续滚动有速度和惯性,手指离开后内容会逐渐停下。

extension XCUIElement {

  // Use the new API for continuous (trackpad-like) scrolling.
  open func swipeUp(velocity: XCUIGestureVelocity = .default)
  open func swipeDown(velocity: XCUIGestureVelocity = .default)
  open func swipeLeft(velocity: XCUIGestureVelocity = .default)
  open func swipeRight(velocity: XCUIGestureVelocity = .default)

}

关键点:

  • swipeUp(velocity:) 合成向上滑动。
  • swipeDown(velocity:) 合成向下滑动。
  • swipeLeft(velocity:) 合成向左滑动。
  • swipeRight(velocity:) 合成向右滑动。
  • velocity 类型是 XCUIGestureVelocity
  • 速度可以使用预设值,也可以使用自定义小数值,单位是每秒像素。

核心启发

  1. 做什么:给 iPad 侧边栏、分栏导航、画布应用补指针测试。 为什么值得做:Xcode 13 可以在 UI 测试中执行两指滚动、悬停和点击,指针专属路径不用再靠人工验收。 怎么开始:在 UI 测试里用 XCTSkipUnless(XCUIDevice.shared.supportsPointerInteraction, ...) 保护测试,再对目标元素调用 scroll(byDeltaX:deltaY:)hover()

  2. 做什么:为 iPad 右键菜单写回归测试。 为什么值得做:指针 API 支持 rightClick(),可以覆盖上下文菜单是否出现、菜单项是否可点。 怎么开始:找到触发菜单的 XCUIElement,调用 rightClick(),再断言菜单中的按钮或文本存在。

  3. 做什么:给 watchOS 表冠调节界面写边界测试。 为什么值得做rotateDigitalCrown(delta:velocity:) 可以指定旋转方向和圈数,适合测试时间轴、音量、温度、计时器这类表冠驱动界面。 怎么开始:在 watchOS UI 测试里启动 App,读取当前标签,调用 XCUIDevice.shared.rotateDigitalCrown(delta:),再检查标签或数值变化。

  4. 做什么:给 macOS 长列表或时间线补触控板滚动测试。 为什么值得做:新的 swipeUpswipeDownswipeLeftswipeRight 更接近触控板输入,可以覆盖惯性滚动下的加载和选择行为。 怎么开始:选中滚动容器元素,调用 swipeUp(velocity:)swipeDown(velocity:),检查目标内容是否出现。

  5. 做什么:测试带修饰键的指针操作。 为什么值得做perform(withKeyModifiers:block:) 可以在 UI 测试中按住修饰键执行点击或拖拽,适合文件选择、范围选择、画布编辑器。 怎么开始:把 click() 或拖拽操作放进 XCUIElement.perform(withKeyModifiers:block:) 的闭包中,再断言选择状态变化。

关联 Session

评论

GitHub Issues · utterances