1

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()
    }
}
Marco Boerner
  • 1,243
  • 1
  • 11
  • 34

1 Answers1

0

From what I can see it looks like you are NOT fulfilling the XCTestExpectation you set up with:

let exp = expectation(description: "Waiting...")

In the past, I have used the XCTestExpecation for asynchronous code to signal completion. So like in this example where I set up the XCTestExpectation and provide a description, then made a network call, and on completion of the network call ran exp.fulfill().

let exp = expectation(description: "Recieved success message after uploading onboarding model")
            
 _ = UserController.upsertUserOnboardingModel().subscribe(onNext: { (response) in
    
   expectation.fulfill()
    
   }, onError: { (error) in
    
      print("\(#function): \(error)")
      XCTFail()
    
   })
    
wait(for: [exp], timeout: 10)

Steps should be: Create Expectation -> Fulfil Expectation within X seconds. I don't see any exp.fulfill() in your provided code so looks like that is a missing step.

Proposed Solutions:

A. So what you could do is fulfill your exp at some point during the number of seconds specified in the timeout: x.

B. Or you want just wanting a delay you could use sleep(3)

C. Or if you want to wait for existence pick an element and use .waitForExistence(timeout: Int) Example: XCTAssert(app.searchFields["Search for cars here"].waitForExistence(timeout: 5))

Morssel
  • 38
  • 8
  • To A. and B. the XCTWaiter I've setup does nothing more than sleep(), replacing it with sleep does not change the result at all. :/ See here: https://stackoverflow.com/a/65657873/12764795 C. If I wait for existence it doesn't solve the problem either. Assuming there are other waitForExistence or delays before that test will also fail at the same place. – Marco Boerner Jan 19 '21 at 20:50