NSOpenPanel 在 macOS 上中断 UI 测试

NSOpenPanel Breaks UI Testing on macOS

我正在使用 Xcode 在具有 com.apple.security.files.user-selected.read-write 授权的沙盒 macOS 应用程序上进行 UI 测试(即,可以访问用户通过一个 NSOpenPanel GUI).

我注意到代码覆盖率在以模态方式呈现打开的面板后立即停止。这是我的代码:

@IBAction func go(_ sender: Any) {

    let panel = NSOpenPanel()
    panel.canCreateDirectories = true
    panel.canChooseDirectories = true
    panel.canChooseFiles = false
    panel.allowsMultipleSelection = false

    let response = panel.runModal()

    switch response {
    case NSApplication.ModalResponse.OK:
        openPanelDidSelectURL(panel.urls[0])

    default:
        return
    }
}

(我已经记录了我的 UI 测试,以便立即接受 NSOpenPanel,选择打开它的文件夹。)

代码覆盖率最终突出显示如下:

我已经尝试用 fatalError() 调用替换 switch 语句,但 UI 测试仍然成功完成,提示紧接着:

let response = panel.runModal()

...在测试期间执行。

禁用沙箱似乎没有效果,所以我怀疑是 运行 模态打开的面板导致了麻烦...

我尝试了所有其他可用的方法来呈现打开的面板,即:

panel.begin { (response) in
    switch response {
    case NSApplication.ModalResponse.OK:
        self.openPanelDidSelectURL(panel.urls[0])

    default:
        return
    }
}

...还有:

panel.beginSheetModal(for: view.window!) { (response) in
    switch response {
    case NSApplication.ModalResponse.OK:
        self.openPanelDidSelectURL(panel.urls[0])

    default:
        return
    }
}

...但结果始终相同: 测试期间不涵盖显示面板后立即执行的所有代码。


最后,我意识到我的 UI 测试不能依赖于打开面板所在的任何用户可选择的文件夹(上次访问的目录?) ,所以我选择使用 mocking

首先,在我的UI测试中类,我采用了这个设置逻辑:

override func setUp() {
    continueAfterFailure = false
    let app = XCUIApplication()
    app.launchArguments.append("-Testing")
    app.launch()
}

("Testing" 之前的连字符是强制性的,否则我基于文档的 macOS 应用程序会认为我启动它是为了打开一个名为 "Testing" 的文档, 并且没有这样做)

接下来,在应用端,我定义了一个全局计算属性来确定我们是否运行在测试中:

public var isTesting: Bool {
    return ProcessInfo().arguments.contains("-Testing")
}

最后,也是在应用程序方面 我将所有 NSOpenPanel 调用包装成两种方法:一种用于提示用户输入要读取的文件,另一种用于提示用于写入结果文件的输出目录的用户(这是我的应用程序在 NSOpenPanel 中的全部需求):

public func promptImportInput(completionHandler: @escaping (([URL]) -> Void)) {
    guard isTesting == false else {
        /* 
          Always returns the URLs of the bundled resource files: 
           - 01@2x.png, 
           - 02@2x.png, 
           - 03@2x.png,
             ...
           - 09@2x.png, 
         */
        let urls = (1 ... 9).compactMap { (index) -> URL? in
            let fileName = String(format: "%02d", index) + "@2x"
            return Bundle.main.url(forResource: fileName, withExtension: "png")
        }
        return completionHandler(urls)   
    }
    // (The code below cannot be covered during automated testing)

    let panel = NSOpenPanel()
    panel.canChooseFiles = true
    panel.canChooseDirectories = true
    panel.canCreateDirectories = false
    panel.allowsMultipleSelection = true

    let response = panel.runModal()

    switch response {
    case NSApplication.ModalResponse.OK:
        completionHandler(panel.urls)
    default:
        completionHandler([])
    }
}

public func promptExportDestination(completionHandler: @escaping((URL?) -> Void)) {
    guard isTesting == false else {
        // Testing: write output to the temp directory 
        // (works even on sandboxed apps):
        let tempPath = NSTemporaryDirectory()
        return completionHandler(URL(fileURLWithPath: tempPath))
    }
    // (The code below cannot be covered during automated testing)

    let panel = NSOpenPanel()
    panel.canChooseFiles = false
    panel.canChooseDirectories = true
    panel.canCreateDirectories = true
    panel.allowsMultipleSelection = false

    let response = panel.runModal()

    switch response {
    case NSApplication.ModalResponse.OK:
        completionHandler(panel.urls.first)
    default:
        completionHandler(nil)
    }
}

这两个函数中使用实际 NSOpenPanel 而不是模拟用户选择 files/directories 的部分仍然被排除 收集代码覆盖率统计信息(但这一次,这是设计使然)。

不过至少现在只有这两个地方了。我的其余代码只是调用这两个函数,不再直接与 NSOpenPanel 交互。 'abstracted' OS 的文件浏览界面远离我的应用程序...