I want to use UI testing for a game using SKSpriteKit
.
As my first tries did not work I wonder if it possible to use Xcode UI Testing with SpriteKit.
I want to use UI testing for a game using SKSpriteKit
.
As my first tries did not work I wonder if it possible to use Xcode UI Testing with SpriteKit.
The main idea is to create the accessibility material for elements that you want to UI test. That's mean:
List all accessible elements contained in the scene
Configure settings for each of these elements, especially frame
data.
This answer is for Swift 3 and is mainly based on Accessibility (Voice Over) with Sprite Kit
Let's say I want to make the SpriteKit button named tapMe
accessible.
Add an array of UIAccessibilityElement
to the Scene.
var accessibleElements: [UIAccessibilityElement] = []
I need to update two methods: didMove(to:)
and willMove(from:)
.
override func didMove(to view: SKView) {
isAccessibilityElement = false
tapMe.isAccessibilityElement = true
}
As scene is the accessibility controller, documentation stated it must return False
to isAccessibilityElement
.
And:
override func willMove(from view: SKView) {
accessibleElements.removeAll()
}
3 methods are involved: accessibilityElementCount()
, accessibilityElement(at index:)
and index(ofAccessibilityElement
. Please allow me to introduce an initAccessibility()
method I'll describe later.
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)
}
}
UIAccessibilityElement
for tapMe
frame
's origin is the top/left corner for UIKitUIAccessibilityElement
UIAccessibilityElement
to list of all accessible elements in scene.Now tapMe
is accessible from UI testing perspective.
According to Apple developer forum discussion, Integrate UITest with SpriteKit is not currently possible:
This is likely not currently possible, but it probably could be
According to comment by @ChrisLivdahl this may be achieved by using UIAccessibility — Session 406, UI Testing in Xcode, WWDC 2015.
The idea is to make the element needed UI Testable.
A much shorter solution tested with SpriteKit, SwiftUI App and Swift 5.4
The earlier approaches didn't seem to work anymore when using XCUI
Tests. The basis of my app is a SwiftUI app that has a SKScene
as its main view. In order for it to work it was at the end quite simple actually and required much less steps for me to work.
1. Deactivate accessibility of the scene by adding only one line to the didMove()
method
override func didMove(to view: SKView) {
isAccessibilityElement = false
}
As mentioned in Dominique Vial's answer
2. Conforming your nodes to UIAccessibilityIdentification protocol
class YourNode: SKSpriteNode, UIAccessibilityIdentification {
var accessibilityIdentifier: String?
//...
}
Mentioned here
Any node that needs to be accessible by the UI Test needs to conform to this protocol. Update Extension, instead of subclassing every node I'm using now the extension below.
3. Assign the accessibilityIdentifier and activate accessibility of your nodes objects.
let yourNode = YourNode()
yourNode.isAccessibilityElement = true
yourNode.accessibilityIdentifier = "nodeID"
4. That's it! Run your tests!
func testingNodes() throws {
app = XCUIApplication()
app.launch()
let node = app.otherElements["nodeID"]
XCTAssert(node.waitForExistence(timeout: 1))
}
5. Optional: set accessibilityTraits
yourNode.accessibilityTraits = [.button, .updatesFrequently]
let nodeAsButton = app.buttons["nodeID"]
You can set specific trait like .button
to tell the accessibility API what your node is. This is useful to differentiate your nodes better during testing but also if you're planning to implement actual accessibility features for the user than this should be set correctly for it to work.
Update 1 - Using Extensions:
Instead of subclassing the nodes I'm using now the following extension on SKNode
. I now only set the accessibilityLabel
an no longer the accessibilityIdentifier
extension SKNode: UIAccessibilityIdentification {
public var accessibilityIdentifier: String? {
get {
super.accessibilityLabel
}
set(accessibilityIdentifier) {
super.accessibilityLabel = accessibilityIdentifier
}
}
}
Update 2 - Making all descendants of SKScene accessible
To allow for nodes and all of their descendants to be accessible I've ended up using the following extension on SKNode. This adds each node to the scene's 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)
}
}
}
}
I've implemented an 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.
Hope this example helps for anyone who is interested in this topic :)
© 2022 - 2024 — McMap. All rights reserved.