NSOpenPanel打破了macOS上的UI测试

NSOpenPanel打破了macOS上的UI测试,macos,xcode-ui-testing,nsopenpanel,Macos,Xcode Ui Testing,Nsopenpanel,我正在使用Xcode在沙盒macOS应用程序上进行UI测试,该应用程序具有com.apple.security.files.user selected.read-write权限(即,可以通过NSOpenPanelGUI访问用户明确选择的文件和文件夹) 我注意到,代码覆盖率在打开面板以MODALY方式显示之后立即停止。这是我的代码: @IBAction func go(_ sender: Any) { let panel = NSOpenPanel() panel.canCrea

我正在使用Xcode在沙盒macOS应用程序上进行UI测试,该应用程序具有
com.apple.security.files.user selected.read-write
权限(即,可以通过
NSOpenPanel
GUI访问用户明确选择的文件和文件夹)

我注意到,代码覆盖率在打开面板以MODALY方式显示之后立即停止。这是我的代码:

@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测试不能依赖于某个用户可选择的文件夹出现在打开面板的任何地方(上次访问的目录?),因此我选择使用模拟

首先,在我的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
中所需的全部内容):

这两个函数中使用实际的
NSOpenPanel
而不是模拟用户选择的文件/目录的部分仍然被排除在收集代码覆盖率统计数据的范围之外(但这一次是出于设计)


但至少现在只有这两个地方。我的其余代码只调用这两个函数,不再直接与
NSOpenPanel
交互。我已经将操作系统的文件浏览界面从我的应用程序中“抽象”出来

是否
查看.window?.close()
释放
self
?@Willeke它不应该;AppDelegate将窗口控制器存储在非弱属性中;窗口控制器应一直有效,直到将新实例分配给属性。。。?此外,当不运行测试时(即调试时),应用程序也能按预期工作。@Willeke在任何情况下,都会删除整个switch子句(
window.close()
包括在内),并用调用
fatalError()
来替换它,因为它什么也不做(即测试不会受到伤害地完成)。与窗口关闭无关…如果它在主线程上被调用,您能签入代码吗?模型对话框需要这一点,并且通常不会强制执行。当从Xcode启动时(另请参阅),以及当应用程序未正确签名时(请参阅)@mahaltertin我的应用程序已签名并已沙盒,我在调试构建中也遇到了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)
    }
}