是否可以在使用 SpriteKit 的应用程序上使用 Xcode UI 测试?

Is it possible to use Xcode UI Testing on an app using SpriteKit?

我想使用 UI 测试使用 SKSpriteKit 的游戏。

由于我的第一次尝试没有成功,我想知道是否可以使用 Xcode UI Testing with SpriteKit。

根据 Apple developer forum discussion,目前无法将 UITest 与 SpriteKit 集成:

This is likely not currently possible, but it probably could be

2017-02-19更新

根据@ChrisLivdahl 的评论,这可以通过使用 UIAccessibility — Session 406, UI Testing in Xcode, WWDC 2015.

来实现

想法是使所需的元素UI可测试。

主要思想是为要 UI 测试的元素创建可访问性 material。这意味着:

  1. 列出场景中包含的所有可访问元素

  2. 配置每个元素的设置,尤其是 frame数据。

循序渐进

此答案针对Swift 3,主要基于

假设我想让名为 tapMe 的 SpriteKit 按钮可访问。

可访问元素列表。

UIAccessibilityElement 的数组添加到场景中。

 var accessibleElements: [UIAccessibilityElement] = []

场景的循环寿命

我需要更新两个方法:didMove(to:)willMove(from:)

override func didMove(to view: SKView) {
    isAccessibilityElement       = false
    tapMe.isAccessibilityElement = true
}

由于场景是辅助功能控制器,文档说明它必须 return FalseisAccessibilityElement

并且:

override func willMove(from view: SKView) {
    accessibleElements.removeAll()
}

覆盖 UIAccessibilityContainer 方法

涉及3种方法:accessibilityElementCount()accessibilityElement(at index:)index(ofAccessibilityElement。请允许我介绍一个initAccessibility()方法,我稍后会描述。

override func accessibilityElementCount() -> Int {
    initAccessibility()
    return accessibleElements.count
}

override func accessibilityElement(at index: Int) -> Any? {

    initAccessibility()
    if (index < accessibleElements.count) {
        return accessibleElements[index]
    } else {
        return nil
    }
}

override func index(ofAccessibilityElement element: Any) -> Int {
    initAccessibility()
    return accessibleElements.index(of: element as! UIAccessibilityElement)!
}

初始化场景的辅助功能

func initAccessibility() {

    if accessibleElements.count == 0 {

        // 1.
        let elementForTapMe   = UIAccessibilityElement(accessibilityContainer: self.view!)

        // 2.
        var frameForTapMe = tapMe.frame

        // From Scene to View
        frameForTapMe.origin = (view?.convert(frameForTapMe.origin, from: self))!

        // Don't forget origins are different for SpriteKit and UIKit:
        // - SpriteKit is bottom/left
        // - UIKit is top/left
        //              y
        //  ┌────┐       ▲
        //  │    │       │   x
        //  ◉────┘       └──▶
        //
        //                   x
        //  ◉────┐       ┌──▶
        //  │    │       │
        //  └────┘     y ▼
        //
        // Thus before the following conversion, origin value indicate the bottom/left edge of the frame.
        // We then need to move it to top/left by retrieving the height of the frame.
        //


        frameForTapMe.origin.y = frameForTapMe.origin.y - frameForTapMe.size.height


        // 3.
        elementForTapMe.accessibilityLabel   = "tap Me"
        elementForTapMe.accessibilityFrame   = frameForTapMe
        elementForTapMe.accessibilityTraits  = UIAccessibilityTraitButton

        // 4.
        accessibleElements.append(elementForTapMe)

    }
}
  1. tapMe
  2. 创建 UIAccessibilityElement
  3. 计算设备坐标上的帧数据。不要忘记 frame 的原点是 UIKit
  4. 的 top/left 角
  5. UIAccessibilityElement
  6. 设置数据
  7. 将此 UIAccessibilityElement 添加到场景中所有可访问元素的列表中。

现在可以从 UI 测试角度访问 tapMe

参考资料

我已经实现了 example project based on your (@Domsware's) awesome answer, and I've confirmed this trick works well for both Xcode UI Testing Framework and KIF

希望这个例子对任何对此主题感兴趣的人有所帮助:)

2021 年初更新

使用 SpriteKit、SwiftUI App 和 Swift 5.4

测试的更短的解决方案

使用 XCUI 测试时,早期的方法似乎不再有效。我的应用程序的基础是一个 SwiftUI 应用程序,它的主视图是 SKScene。为了让它工作,它实际上非常简单,而且我需要的工作步骤要少得多。

1.通过仅向 didMove() 方法

添加一行来停用场景的可访问性
override func didMove(to view: SKView) {
    isAccessibilityElement = false
}

所述

2。使您的节点符合 UIAccessibilityIdentification 协议

class YourNode: SKSpriteNode, UIAccessibilityIdentification {
    var accessibilityIdentifier: String?
//...
}

提到here

任何需要被UI测试访问的节点都需要符合这个协议。 更新扩展,而不是将我现在使用的每个节点子类化为下面的扩展。

3。分配 accessibilityIdentifier 并激活节点对象的可访问性。

let yourNode = YourNode()
yourNode.isAccessibilityElement = true
yourNode.accessibilityIdentifier = "nodeID"

4.而已! 运行 你的测试!

func testingNodes() throws {
   app = XCUIApplication()
   app.launch()
   let node = app.otherElements["nodeID"]
   XCTAssert(node.waitForExistence(timeout: 1))
}

5.可选:设置 accessibilityTraits

yourNode.accessibilityTraits = [.button, .updatesFrequently]


let nodeAsButton = app.buttons["nodeID"]

您可以设置特定特征,例如 .button 来告诉可访问性 API 您的节点是什么。这对于在测试期间更好地区分您的节点很有用,而且如果您计划为用户实现实际的可访问性功能,那么应该正确设置它才能工作。

更新 1 - 使用扩展:

我现在使用的不是子类化节点,而是 SKNode 上的以下扩展。我现在只设置 accessibilityLabel 不再是 accessibilityIdentifier

 extension SKNode: UIAccessibilityIdentification {
    public var accessibilityIdentifier: String? {
        get {
            super.accessibilityLabel
        }
        set(accessibilityIdentifier) {
            super.accessibilityLabel = accessibilityIdentifier
        }
    }
}

更新 2 - 使 SKScene 的所有后代都可访问

为了允许访问节点及其所有后代,我最终在 SKNode 上使用了以下扩展。这会将每个节点添加到场景的 accessibilityElements.

extension SKNode: UIAccessibilityIdentification {
    public var accessibilityIdentifier: String? {
        get {
            super.accessibilityLabel
        }
        set(accessibilityIdentifier) {
            super.accessibilityLabel = accessibilityIdentifier
        }
    }

    func makeUITestAccessible(label: String, traits: UIAccessibilityTraits) {

        accessibilityLabel = label
        isAccessibilityElement = true
        accessibilityTraits = traits

        if let scene = scene {
            if scene.accessibilityElements == nil {
                scene.accessibilityElements = [self]
            } else {
                scene.accessibilityElements?.append(self)
            }
        }
    }
}