【问题标题】:Xcode 7 UI Testing: how to dismiss a series of system alerts in codeXcode 7 UI 测试:如何在代码中消除一系列系统警报
【发布时间】:2015-11-15 21:48:49
【问题描述】:

我正在使用新的 Xcode 7 UI 测试功能编写 UI 测试用例。在我的应用程序的某个时刻,我要求用户授予相机访问权限和推送通知。所以会出现两个 iOS 弹出窗口:"MyApp Would Like to Access the Camera" popup 和 "MyApp Would Like to Send You Notifications" popup。我希望我的测试消除两个弹出窗口。

UI 录制为我生成了以下代码:

[app.alerts[@"cameraAccessTitle"].collectionViews.buttons[@"OK"] tap];

但是,[app.alerts[@"cameraAccessTitle"] exists] 解析为 false,并且上面的代码会生成错误:Assertion Failure: UI Testing Failure - Failure getting refresh snapshot Error Domain=XCTestManagerErrorDomain Code=13 "Error copying attributes -25202"

那么在测试中消除一堆系统警报的最佳方法是什么?系统弹出窗口会中断我的应用程序流程并立即使我的正常 UI 测试用例失败。事实上,任何关于如何绕过系统警报以便我可以恢复测试通常流程的建议都值得赞赏。

这个问题可能与这个没有答案的 SO 帖子有关:Xcode7 | Xcode UI Tests | How to handle location service alert?

提前致谢。

【问题讨论】:

  • 一般情况下,您不应同时显示两个警报。但是,如果您在它们之间稍作延迟,您可以确定警报呈现的顺序。
  • 如果我知道序列(它几乎总是相同的序列),我想知道为什么 [app.alerts[cameraAccessTitle].collectionViews.buttons[@"OK"] tap];[app.alerts[notificationAccessTitle].collectionViews.buttons[@"OK"] tap]; 永远不起作用。它将关闭第一个弹出窗口,然后停止。第二次点击没有发生。我将把这个细节添加到我的帖子@ILikeTau
  • 是否不可能在另一个警报出现之前关闭警报?

标签: ios objective-c xctest xcode7 xcode-ui-testing


【解决方案1】:

天啊!我讨厌 XCTest 如何在处理 UIView 警报时遇到最糟糕的情况。我有一个应用程序,我在其中收到 2 个警报,第一个警报要我选择“允许”以启用应用程序权限的位置服务,然后在启动页面上,用户必须按下一个名为“打开位置”的 UIButton,最后有一个UIViewAlert 中的通知短信警报,用户必须选择“确定”。我们遇到的问题是无法与系统警报交互,而且是一种竞争条件,其中行为及其在屏幕上的出现不合时宜。看来,如果您使用alert.element.buttons["whateverText"].tap,XCTest 的逻辑是一直按下直到测试时间用完。所以基本上一直按屏幕上的任何东西,直到所有系统警报都清晰可见。

这是一个 hack,但这对我有用。

func testGetPastTheStupidAlerts() {
    let app = XCUIApplication()
    app.launch()
    
    if app.alerts.element.collectionViews.buttons["Allow"].exists {
        app.tap()
    }

    app.buttons["TURN ON MY LOCATION"].tap()
}

字符串“Allow”被完全忽略,app.tap() 的逻辑在每次看到警报时都被调用,最后我想到达的按钮 [“Turn On Location”] 可以访问并且测试通过

~完全困惑,感谢Apple。

【讨论】:

    【解决方案2】:

    这是一个老问题,但现在有另一种方法来处理这些警报。

    无法从您启动的应用的应用上下文访问系统警报,但无论如何您都可以访问应用上下文。看这个简单的例子:

    func testLoginHappyPath() {
        let app = XCUIApplication()
        app.textFields["Username"].typeText["Billy"]
        app.secureTextFields["Password"].typeText["hunter2"]
        app.buttons["Log In"].tap()
    }
    

    在模拟器已经启动并且权限已经授予或拒绝的情况下,这将起作用。但是如果我们把它放在一个 CI 管道中,它会得到一个全新的模拟器,突然之间它会找不到那个 Username 字段,因为会弹出一个通知警报。

    所以现在有 3 个选择来处理这个问题:

    隐式

    已经有一个默认的系统警报中断处理程序。所以理论上,简单地尝试在第一个字段上键入文本应该检查中断事件并肯定地处理它。

    如果一切都按设计运行,您无需编写任何代码,但您会在日志中看到记录和处理的中断,并且您的测试将需要几秒钟的时间。

    显式通过中断监视器

    我不会重写之前的工作,但这是您明确设置中断监视器以处理弹出的特定警报或您期望发生的任何警报的地方。

    如果内置处理程序不能执行您想要的操作 - 或根本不工作,这很有用。

    显式通过 XCUITest 框架

    在 xCode 9.0 及更高版本中,您可以通过简单地定义多个 XCUIApplication() 实例来流畅地在应用程序上下文之间切换。然后您可以通过熟悉的方法找到您需要的字段。因此,明确地执行此操作如下所示:

    func testLoginHappyPath() {
        let app = XCUIApplication()
        let springboardApp = XCUIApplication(bundleidentifier: "com.apple.springboard")
    
        if springboardApp.alerts[""FunHappyApp" would like permission to own your soul."].exists {
            springboardApp.alerts.buttons["Allow"].tap()
        }
    
        app.textFields["Username"].typeText["Billy"]
        app.secureTextFields["Password"].typeText["hunter2"]
        app.buttons["Log In"].tap()
    }
    

    【讨论】:

      【解决方案3】:

      天哪。 即使我故意说轻按“允许”,它也总是轻按“不允许”

      至少

      if app.alerts.element.collectionViews.buttons["Allow"].exists {
          app.tap()
      }
      

      允许我继续前进并进行其他测试。

      【讨论】:

        【解决方案4】:

        @Joe Masilotti 的回答是正确的,谢谢,它对我帮助很大:)

        我只想指出一件事,那就是 UIInterruptionMonitor 捕获 所有 系列 TOGETHER 中呈现的系统警报,所以您在完成处理程序中应用的操作将应用于每个警报(“不允许”“确定”)。如果您想以不同的方式处理警报操作,则必须在完成处理程序中检查当前显示的是哪个警报,例如通过检查其静态文本,然后该操作将仅应用于该警报。

        这里的小代码 sn-p 用于在第二个警报上应用 “不允许” 操作,在三个警报的系列中,以及 “OK” 操作上剩下的两个:

        addUIInterruptionMonitor(withDescription: "Access to sound recording") { (alert) -> Bool in
                if alert.staticTexts["MyApp would like to use your microphone for recording your sound."].exists {
                    alert.buttons["Don’t Allow"].tap()
                } else {
                    alert.buttons["OK"].tap()
                }
                return true
            }
        app.tap()
        

        【讨论】:

          【解决方案5】:

          对于那些正在寻找特定系统对话框的特定描述的人(就像我所做的那样),没有 :) 该字符串仅用于测试人员跟踪目的。相关苹果文档链接:https://developer.apple.com/documentation/xctest/xctestcase/1496273-adduiinterruptionmonitor


          更新:xcode 9.2

          该方法有时触发有时不触发。对我来说最好的解决方法是当我知道会有系统警报时,我补充说:

          sleep(2)
          app.tap()
          

          系统警报消失了

          【讨论】:

            【解决方案6】:

            在 xcode 9.1 上,只有在测试设备装有 iOS 11 时才会处理警报。不适用于较旧的 iOS 版本,例如 10.3 等。参考:https://forums.developer.apple.com/thread/86989

            要处理警报,请使用:

            //Use this before the alerts appear. I am doing it before app.launch()
            
            let allowButtonPredicate = NSPredicate(format: "label == 'Always Allow' || label == 'Allow'")
            //1st alert
            _ = addUIInterruptionMonitor(withDescription: "Allow to access your location?") { (alert) -> Bool in
                let alwaysAllowButton = alert.buttons.matching(allowButtonPredicate).element.firstMatch
                if alwaysAllowButton.exists {
                    alwaysAllowButton.tap()
                    return true
                }
                return false
            }
            //Copy paste if there are more than one alerts to handle in the app
            

            【讨论】:

              【解决方案7】:

              我发现唯一能可靠解决此问题的是设置两个单独的测试来处理警报。在第一个测试中,我打电话给app.tap(),什么也不做。在第二个测试中,我再次调用app.tap(),然后进行真正的工作。

              【讨论】:

              • 它很愚蠢,但它也是唯一对我有用的东西。截至 17 年 20 月 10 日,此处没有其他答案。谢谢。
              【解决方案8】:

              目标 - C

              -(void) registerHandlerforDescription: (NSString*) description {
              
                  [self addUIInterruptionMonitorWithDescription:description handler:^BOOL(XCUIElement * _Nonnull interruptingElement) {
              
                      XCUIElement *element = interruptingElement;
                      XCUIElement *allow = element.buttons[@"Allow"];
                      XCUIElement *ok = element.buttons[@"OK"];
              
                      if ([ok exists]) {
                          [ok tap];
                          return YES;
                      }
              
                      if ([allow exists]) {
                          [allow tap];
                          return YES;
                      }
              
                      return NO;
                  }];
              }
              
              -(void)setUp {
              
                  [super setUp];
              
                  self.continueAfterFailure = NO;
                  self.app = [[XCUIApplication alloc] init];
                  [self.app launch];
              
                  [self registerHandlerforDescription:@"“MyApp” would like to make data available to nearby Bluetooth devices even when you're not using app."];
                  [self registerHandlerforDescription:@"“MyApp” Would Like to Access Your Photos"];
                  [self registerHandlerforDescription:@"“MyApp” Would Like to Access the Camera"];
              }
              

              斯威夫特

              addUIInterruptionMonitorWithDescription("Description") { (alert) -> Bool in
                  alert.buttons["Allow"].tap()
                  alert.buttons["OK"].tap()
                  return true
              }
              

              【讨论】:

              • 我对目标 C 示例有点困惑:为什么要注册 3 个处理程序?一个还不够吗?
              • @Leo 这些就是例子。您可以根据需要添加尽可能多或尽可能少的内容。
              【解决方案9】:

              Xcode 7.1

              Xcode 7.1 终于修复了系统警报问题。但是,有两个小问题。

              首先,您需要在显示警报之前设置“UI 中断处理程序”。这是我们告诉框架如何处理出现的警报的方式。

              其次,在呈现警报后,您必须与界面进行交互。只需点按应用即可,但这是必需的。

              addUIInterruptionMonitorWithDescription("Location Dialog") { (alert) -> Bool in
                  alert.buttons["Allow"].tap()
                  return true
              }
              
              app.buttons["Request Location"].tap()
              app.tap() // need to interact with the app for the handler to fire
              

              “位置对话框”只是一个字符串,用于帮助开发人员识别访问了哪个处理程序,它并不特定于警报类型。

              我相信从处理程序返回true 会将其标记为“完成”,这意味着它不会再次被调用。对于您的情况,我会尝试返回false,这样第二个警报将再次触发处理程序。

              Xcode 7.0

              以下内容将关闭 Xcode 7 Beta 6 中的单个“系统警报”:

              let app = XCUIApplication()
              app.launch()
              // trigger location permission dialog
              
              app.alerts.element.collectionViews.buttons["Allow"].tap()
              

              Beta 6 引入了一系列针对 UI 测试的修复,我相信这就是其中之一。

              另外请注意,我直接在-alerts 上调用-element。在XCUIElementQuery 上调用-element 会强制框架选择屏幕上的“唯一”匹配元素。这对于您一次只能看到一个的警报非常有用。但是,如果您尝试对一个标签执行此操作并且有两个标签,则框架将引发异常。

              【讨论】:

              • 嗨,乔,感谢您的回答。该行对我的应用程序的作用是 - 警报成功解除,然后测试在同一行失败并出现以下错误:UI Testing Failure - No matches found for Alert
              • 确实如此。我提交了bug report, rdar://22498241。我建议任何遇到这种情况的人复制它。
              • 如果您不告诉它点击任何内容,它将点击“确定”或任何接受值。我还没有找到点击取消或否等的方法。
              • @JoeMasilotti 在中断监视器中返回 false 的建议是否使您能够消除两个系统警报?我正在处理完全相同的事情——此外,我的警报是针对通知和定位服务的,所以按钮是不同的;我需要包括两个中断监视器。该解决方案非常适合单个系统警报,但我无法触发第二个中断监视器。
              • 大家好,如果我的应用中有本地化版本怎么办?在不同的语言下,alert中的应用名称和按钮标题都会有所不同,不仅仅是英文。
              【解决方案10】:

              听起来像您所说的实现相机访问和通知的方法是线程化的,但不是物理管理的,并且在何时以及如何显示它们时留有机会。

              我怀疑一个是由另一个触发的,当以编程方式单击它时,它也会清除另一个(Apple 可能永远不会允许)

              您认为您是在请求用户许可,然后代表他们做出决定吗?为什么?因为你可能无法让你的代码工作。

              如何修复 - 跟踪这两个组件在哪里触发弹出对话框 - 它们在哪里被调用?重写以仅触发一个,当一个对话完成时发送 NSNotification 以触发并显示剩余的一个。

              我强烈反对以编程方式单击用户专用的对话按钮的方法。

              【讨论】:

              • 感谢您的回复!我关闭“为用户”对话框的原因是因为这是一个 UI 测试用例。就像任何其他模拟用户交互的 UI 测试用例一样,我需要我的测试来消除这两个弹出窗口,就像用户会做的那样
              • 好的,我现在明白了 - 两个对话仍然嵌套在一起以完成您的测试,您可能必须将一个与另一个分离。我曾经不得不为位置检查和通知权限做同样的事情。我在应用程序中创建了一个公共区域,该区域从 1 个对话解雇中捕获了通知,然后触发了第二个通知。我会采取这种方法。祝你好运。
              • 嗨,latenitecoder,让我们退后一步,因为我不认为这里的两个弹出窗口是真正的问题。您是否尝试过使用 Xcode UI 测试来消除任何系统弹出窗口?只有一个弹出窗口,而不是嵌套情况。如果是这样,您用来关闭它的代码行是什么?因为现在,我什至无法完成这项工作。我需要回答的很简单——新的 Xcode UI 测试功能是否能够完全消除系统警报?如果是这样,该怎么做?任何地方都没有官方文档。
              猜你喜欢
              • 2016-08-13
              • 2022-06-23
              • 1970-01-01
              • 1970-01-01
              • 2016-03-23
              • 1970-01-01
              • 2016-02-25
              • 1970-01-01
              • 2016-01-12
              相关资源
              最近更新 更多