The app:
The app has a tag cloud that adds and removes tags as Text views every few seconds to a ZStack using ForEach triggered by an ObservableObject. The ZStack has an accessibilityIdentifier set.
The UI test:
In the UI Test I have set a XCTWaiter first. After a certain period of time has passed I then check if the XCUIElement (ZStack) with the accessibilityIdentifier exists.
After that I query the ZStack XCUIElement for all descendants of type .staticText
I also query the XCUIApplication for its descendants of type .staticText
The following issues:
When the XCTWaiter is set too wait too long. It does not find the ZStack XCUIElement with its identifier anymore.
If the XCTWaiter is set to a low wait time or removed the ZStack XCUIElement will be found. But it will never find its descendants of type .staticText. They do exists though because I can find them as descendant of XCUIApplication itself.
My assumption:
I assume that the ZStack with its identifier can only be found by the tests as long as it does not have descendants. And because it doesn't have any descendants at this moment yet, querying the ZStack XCUIElement for its descendants later also fails because the XCUIElement seems to only represent the ZStack at the time it was captured.
Or maybe I have attached the accessibilityIdentifier for the ZStack at the wrong place or SwiftUI is removing it as soon as there are descendants and I should add identifiers to the descendants only. But that would mean I can only query descendants from XCUIApplication itself and never from another XCUIElement? That would make the .children(matching:) quite useless.
Here is the code for a single view iOS app in SwiftUI with tests enabled.
MyAppUITests.swift
import XCTest
class MyAppUITests: XCTestCase {
func testingTheInitialView() throws {
let app = XCUIApplication()
app.launch()
let exp = expectation(description: "Waiting for tag cloud to be populated.")
_ = XCTWaiter.wait(for: [exp], timeout: 1) // 1. If timeout is set higher
let tagCloud = app.otherElements["TagCloud"]
XCTAssert(tagCloud.exists) // 2. The ZStack with the identifier "TagCloud" does not exist anymore.
let tagsDescendingFromTagCloud = tagCloud.descendants(matching: .staticText)
XCTAssert(tagsDescendingFromTagCloud.firstMatch.waitForExistence(timeout: 2)) // 4. However, it never finds the tags as the descendants of the tagCloud
let tagsDescendingFromApp = app.descendants(matching: .staticText)
XCTAssert(tagsDescendingFromApp.firstMatch.waitForExistence(timeout: 2)) // 3. It does find the created tags here.
}
}
ContentView.swift:
import SwiftUI
struct ContentView: View {
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
@ObservedObject var tagModel = TagModel()
var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}.animation(Animation.easeInOut(duration: 4.0))
.accessibilityIdentifier("TagCloud")
}
}
class TagModel: ObservableObject {
@Published var tags = [String]()
func addNextTag() {
tags.append(String( Date().timeIntervalSince1970 ))
}
func removeOldestTag() {
tags.remove(at: 0)
}
}
struct TagView: View {
@State private var show: Bool = true
@State private var position: CGPoint = CGPoint(x: Int.random(in: 50..<250), y: Int.random(in: 100..<200))
let label: String
var body: some View {
let text = Text(label)
.position(position)
.opacity(show ? 0.0 : 1.0)
.onAppear {
show.toggle()
}
return text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}