计时器经过后,SwiftUI 应用程序的 UI 测试未找到设置了可访问性标识符的元素

Posted

技术标签:

【中文标题】计时器经过后,SwiftUI 应用程序的 UI 测试未找到设置了可访问性标识符的元素【英文标题】:UI Test for SwiftUI app does't find element with set accessibilityIdentifier after a timer has passed 【发布时间】:2021-01-19 19:03:02 【问题描述】:

应用:

该应用程序有一个标签云,它使用由ObservableObject 触发的ForEach 每隔几秒添加和删除标签作为Text 视图到ZStackZStack 有一个 accessibilityIdentifier 集。

用户界面测试:

在 UI 测试中,我首先设置了 XCTWaiter。经过一段时间后,我检查XCUIElement (ZStack) 和accessibilityIdentifier 是否存在。 之后,我查询 ZStack XCUIElement 以获取 .staticText 类型的所有后代 我还查询了XCUIApplication.staticText 类型的后代

以下问题:

如果XCTWaiter 设置得太久,请等待太久。它不再找到 ZStack XCUIElement 及其标识符。

如果 XCTWaiter 设置为较短的等待时间或删除了 ZStack XCUIElement 将被发现。但它永远不会找到类型为.staticText 的后代。但它们确实存在,因为我可以找到它们作为 XCUIApplication 本身的后代。

我的假设:

我假设ZStack 及其标识符只能通过测试找到,只要它没有后代。而且因为此时它还没有任何后代,所以稍后查询 ZStack XCUIElement 的后代也会失败,因为 XCUIElement 似乎只代表它被捕获时的 ZStack

或者我可能在错误的位置为ZStack 附加了accessibilityIdentifier,或者SwiftUI 会在有后代时立即删除它,我应该只为后代添加标识符。但这意味着我只能从XCUIApplication 本身查询后代,而不能从另一个 XCUIElement 查询?这将使.children(matching:) 变得毫无用处。

这是启用了测试的 SwiftUI 中单视图 iOS 应用的代码。

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()
    

【问题讨论】:

【参考方案1】:

据我所知,您似乎没有满足您设置的 XCTestExpectation:

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

在过去,我使用 XCTestExpecation 异步代码来表示完成。因此,就像在这个示例中,我设置了 XCTestExpectation 并提供了描述,然后进行了网络调用,并在网络调用完成后运行 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)

步骤应该是:创建期望 -> 在 X 秒内实现期望。 我在您提供的代码中没有看到任何exp.fulfill(),所以看起来这是一个缺失的步骤。

建议的解决方案:

A.所以你可以做的是在某个时间点完成你的exp timeout: x 中指定的秒数。

B.或者您只想延迟可以使用sleep(3)

C.或者如果你想等待存在选择一个元素并使用.waitForExistence(timeout: Int) 示例:XCTAssert(app.searchFields["Search for cars here"].waitForExistence(timeout: 5))

【讨论】:

对 A. 和 B. 我设置的 XCTWaiter 只不过是 sleep(),用 sleep 替换它根本不会改变结果。 :/ 见这里:***.com/a/65657873/12764795 C. 如果我等待存在它也不能解决问题。假设在该测试之前还有其他 waitForExistence 或延迟也会在同一个地方失败。

以上是关于计时器经过后,SwiftUI 应用程序的 UI 测试未找到设置了可访问性标识符的元素的主要内容,如果未能解决你的问题,请参考以下文章

Timer 计时器 (SwiftUI中文文档手册)

SwiftUI:在设备方向上重绘会使计时器无效

SwiftUI:具有计时器样式的文本在提供的日期更改后不会更新

SwiftUI:取消定时器后如何重新启动它?

为啥将 ViewModel 添加到我的 SwiftUI 应用程序后我的 UI 没有更新?

SwiftUI - 倒计时结束后,跳转到另一个视图